Skip to content

Commit 4fb38a1

Browse files
authored
增加了we协议可选
1 parent 9c74d90 commit 4fb38a1

1 file changed

Lines changed: 186 additions & 36 deletions

File tree

汤盛碗的clash一键配置生成器.html

Lines changed: 186 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
<!DOCTYPE html>
23
<html lang="zh-CN">
34
<head>
@@ -40,7 +41,9 @@
4041
/* 列表 */
4142
.node-list { margin-top: 20px; border-top: 1px solid #eee; padding-top: 20px; }
4243
.node-item { background: #f5f5f7; padding: 10px 15px; border-radius: 8px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem; }
43-
.node-delete { color: #ff3b30; cursor: pointer; padding: 5px; margin-left: 10px; font-weight: bold; }
44+
.node-info { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 10px; }
45+
.node-delete { color: #ff3b30; cursor: pointer; padding: 5px; font-weight: bold; font-size: 1.2rem; line-height: 1; }
46+
.node-delete:hover { opacity: 0.7; }
4447

4548
.tip { font-size: 0.85rem; color: #86868b; margin-top: 10px; background: #f5f5f7; padding: 12px; border-radius: 10px; line-height: 1.6; }
4649
.warning-text { color: #d00; font-weight: 600; }
@@ -89,12 +92,27 @@ <h1>汤盛碗的Clash 配置生成器</h1>
8992
</div>
9093
<div class="form-group">
9194
<label>UUID / 密码</label>
92-
<input type="text" id="uuid">
95+
<input type="text" id="uuid" placeholder="Vmess/Vless/Hy2 填UUID/密码">
96+
</div>
97+
98+
<div class="form-group" style="display:flex; gap:10px;">
99+
<div style="flex:1;">
100+
<label>加密/Cipher (仅SS/Vmess)</label>
101+
<input type="text" id="cipher" placeholder="如 auto, aes-256-gcm">
102+
</div>
103+
<div style="flex:1;">
104+
<label>传输/Network (可选)</label>
105+
<select id="network" style="height:46px;">
106+
<option value="tcp">TCP</option>
107+
<option value="ws">WebSocket (WS)</option>
108+
</select>
109+
</div>
93110
</div>
111+
94112
<div class="form-group">
95113
<label>类型</label>
96114
<select id="type">
97-
<option value="vless">VLESS</option>
115+
<option value="vless">VLESS (默认TLS)</option>
98116
<option value="vmess">VMess</option>
99117
<option value="ss">Shadowsocks</option>
100118
<option value="hysteria2">Hysteria2</option>
@@ -105,6 +123,7 @@ <h1>汤盛碗的Clash 配置生成器</h1>
105123
<button class="btn btn-success" onclick="generateFromList()">🚀 生成</button>
106124
</div>
107125
<div class="node-list" id="nodeListContainer" style="display:none;">
126+
<div style="font-size:0.8rem; color:#86868b; margin-bottom:10px;">已添加节点 (点击生成导出):</div>
108127
<div id="nodeList"></div>
109128
</div>
110129
</div>
@@ -138,7 +157,6 @@ <h1>汤盛碗的Clash 配置生成器</h1>
138157
if (!input) { alert("⚠️ 请粘贴内容!"); return; }
139158

140159
// 1. 尝试作为链接下载
141-
// 排除掉明确是单行节点链接的协议,避免误入下载逻辑
142160
if (input.startsWith('http') && !input.startsWith('hysteria2://') && !input.startsWith('vless://') && !input.startsWith('vmess://') && !input.startsWith('ss://')) {
143161
const btn = document.querySelector('#mode-batch .btn');
144162
const originalText = btn.innerText;
@@ -173,13 +191,15 @@ <h1>汤盛碗的Clash 配置生成器</h1>
173191
if (yamlProxies && yamlProxies.length > 0) {
174192
proxies = yamlProxies;
175193
} else {
176-
// 策略 B: 如果不是 YAML,尝试解析 Base64 或 vless:// 链接
177-
// 【重要修复】增加判断,如果是单行协议链接,绝对不要尝试 Base64 解码,否则会报错导致脚本中断
194+
// 策略 B: 如果不是 YAML,尝试解析 Base64
178195
const isProtocolLink = content.startsWith('vless://') || content.startsWith('hysteria2://') || content.startsWith('vmess://') || content.startsWith('ss://');
179196

180197
if (!content.includes('proxies:') && !content.includes('- {') && !isProtocolLink) {
181198
const decoded = safeBase64Decode(content);
182-
if (decoded) content = decoded;
199+
// 只有当解码内容包含协议关键字时才采纳
200+
if (decoded && (decoded.includes('://') || decoded.includes('proxies:') || decoded.includes('server:'))) {
201+
content = decoded;
202+
}
183203
}
184204

185205
const lines = content.split(/[\r\n]+/);
@@ -190,36 +210,38 @@ <h1>汤盛碗的Clash 配置生成器</h1>
190210
if (line.startsWith('vless://')) result = parseVless(line);
191211
else if (line.startsWith('hysteria2://')) result = parseHysteria2(line);
192212
else if (line.startsWith('vmess://')) result = parseVmess(line);
193-
else if (line.startsWith('ss://')) result = parseSS(line);
213+
else if (line.startsWith('ss://')) result = parseSS(line);
194214

195215
if (result) {
196216
let finalName = result.name;
197217
if (nameCount[finalName]) {
198218
nameCount[finalName]++;
199219
finalName = `${finalName} (${nameCount[finalName]})`;
200-
result.config = result.config.replace(/name:\s*["'].*?["']/, `name: "${finalName}"`);
220+
221+
// 即使重名,替换时也要注意转义
222+
const safeName = finalName.replace(/"/g, '\\"');
223+
result.config = result.config.replace(/name:\s*["'].*?["']/, `name: "${safeName}"`);
224+
201225
result.name = finalName;
202226
} else { nameCount[finalName] = 1; }
203227
proxies.push(result);
204228
}
205229
});
206230
}
207231

208-
// --- 核心过滤 (Fix Error) ---
232+
// --- 核心过滤 ---
209233
proxies = proxies.filter(p => {
210234
if (!p || !p.config) return false;
211235
const configStr = p.config.toLowerCase();
212236
const n = p.name.toLowerCase();
213237

214-
// 1. [关键] 剔除策略组类型
215238
if (configStr.includes('type: select') ||
216239
configStr.includes('type: url-test') ||
217240
configStr.includes('type: fallback') ||
218241
configStr.includes('type: load-balance')) {
219242
return false;
220243
}
221244

222-
// 2. 剔除广告/无效节点
223245
if (n.includes('剩余') || n.includes('流量') || n.includes('到期') ||
224246
n.includes('官网') || n.includes('expire') || n.includes('reset') ||
225247
n.includes('🪧') || n.includes('不可用')) {
@@ -234,11 +256,10 @@ <h1>汤盛碗的Clash 配置生成器</h1>
234256
return;
235257
}
236258

237-
// 生成文件
238259
downloadConfig(proxies.map(p => p.config), proxies.map(p => p.name));
239260
}
240261

241-
// --- 提取 Inline YAML (- { name: ... }) ---
262+
// --- 提取 Inline YAML ---
242263
function extractInlineYamlProxies(content) {
243264
try {
244265
const nodes = [];
@@ -262,7 +283,7 @@ <h1>汤盛碗的Clash 配置生成器</h1>
262283
if (nameCount[name]) {
263284
nameCount[name]++;
264285
const newName = `${name} (${nameCount[name]})`;
265-
config = config.replace(name, newName);
286+
config = config.replace(name, newName);
266287
name = newName;
267288
} else {
268289
nameCount[name] = 1;
@@ -275,29 +296,29 @@ <h1>汤盛碗的Clash 配置生成器</h1>
275296
} catch (e) { console.error(e); return []; }
276297
}
277298

278-
// --- [修复] 辅助工具:增加错误捕获,防止解码失败导致程序崩溃 ---
279299
function safeBase64Decode(str) {
280300
try {
281301
str = str.replace(/\s/g, '');
282302
return decodeURIComponent(escape(atob(str.replace(/-/g, '+').replace(/_/g, '/'))));
283303
} catch (e) {
284-
// 如果解码失败,返回 null 或者原字符串,保证程序继续运行
285-
console.warn("Base64 Decode Failed, treating as raw text.");
286304
return null;
287305
}
288306
}
289307

290-
// --- 解析函数 ---
308+
// --- 解析函数 (名称转义 + 端口兜底) ---
291309
function parseVless(line) {
292310
try {
293311
const u = new URL(line);
294312
const params = u.searchParams;
295-
const name = decodeURIComponent(u.hash.slice(1)) || u.hostname;
313+
const rawName = decodeURIComponent(u.hash.slice(1)) || u.hostname;
314+
const safeName = rawName.replace(/"/g, '\\"');
296315
const type = params.get('type') || 'tcp';
297-
let config = ` - name: "${name}"
316+
const port = u.port || 443;
317+
318+
let config = ` - name: "${safeName}"
298319
type: vless
299320
server: ${u.hostname}
300-
port: ${u.port}
321+
port: ${port}
301322
uuid: ${u.username}
302323
tls: true
303324
udp: true
@@ -306,19 +327,22 @@ <h1>汤盛碗的Clash 配置生成器</h1>
306327
network: ${type}`;
307328
if (type === 'ws') config += `\n ws-opts:\n path: "${params.get('path')||'/'}"\n headers:\n Host: ${params.get('sni')||u.hostname}`;
308329
if (params.get('flow')) config += `\n flow: ${params.get('flow')}`;
309-
return { name: name, config: config };
330+
return { name: rawName, config: config };
310331
} catch (e) { return null; }
311332
}
312333

313334
function parseHysteria2(line) {
314335
try {
315336
const u = new URL(line);
316337
const params = u.searchParams;
317-
const name = decodeURIComponent(u.hash.slice(1)) || u.hostname;
318-
let config = ` - name: "${name}"
338+
const rawName = decodeURIComponent(u.hash.slice(1)) || u.hostname;
339+
const safeName = rawName.replace(/"/g, '\\"');
340+
const port = u.port || 443;
341+
342+
let config = ` - name: "${safeName}"
319343
type: hysteria2
320344
server: ${u.hostname}
321-
port: ${u.port}
345+
port: ${port}
322346
password: ${u.username}
323347
sni: ${params.get('sni') || u.hostname}
324348
skip-cert-verify: true
@@ -327,7 +351,7 @@ <h1>汤盛碗的Clash 配置生成器</h1>
327351
if (params.get('obfs') && params.get('obfs') !== 'none') {
328352
config += `\n obfs: ${params.get('obfs')}\n obfs-password: "${params.get('obfs-password')||''}"`;
329353
}
330-
return { name: name, config: config };
354+
return { name: rawName, config: config };
331355
} catch (e) { return null; }
332356
}
333357

@@ -337,13 +361,16 @@ <h1>汤盛碗的Clash 配置生成器</h1>
337361
const jsonStr = safeBase64Decode(b64);
338362
if (!jsonStr) return null;
339363
const c = JSON.parse(jsonStr);
340-
const name = c.ps || c.add;
364+
const rawName = c.ps || c.add;
365+
const safeName = rawName.replace(/"/g, '\\"');
366+
const port = c.port || 443;
367+
341368
return {
342-
name: name,
343-
config: ` - name: "${name}"
369+
name: rawName,
370+
config: ` - name: "${safeName}"
344371
type: vmess
345372
server: ${c.add}
346-
port: ${c.port}
373+
port: ${port}
347374
uuid: ${c.id}
348375
alterId: ${c.aid || 0}
349376
cipher: ${c.scy || 'auto'}
@@ -357,13 +384,135 @@ <h1>汤盛碗的Clash 配置生成器</h1>
357384
} catch (e) { return null; }
358385
}
359386

360-
function parseSS(line) { return null; }
387+
function parseSS(line) {
388+
try {
389+
let raw = line.replace('ss://', '');
390+
let rawName = 'Shadowsocks Node';
391+
if (raw.includes('#')) {
392+
const parts = raw.split('#');
393+
raw = parts[0];
394+
rawName = decodeURIComponent(parts[1]);
395+
}
396+
const safeName = rawName.replace(/"/g, '\\"');
397+
398+
let decoded = raw;
399+
if (!raw.includes('@')) {
400+
decoded = safeBase64Decode(raw);
401+
if(!decoded) return null;
402+
}
403+
404+
const lastAt = decoded.lastIndexOf('@');
405+
if (lastAt === -1) return null;
406+
407+
const userPass = decoded.substring(0, lastAt);
408+
const serverPort = decoded.substring(lastAt + 1);
409+
410+
const firstColon = userPass.indexOf(':');
411+
const method = userPass.substring(0, firstColon);
412+
const password = userPass.substring(firstColon + 1);
413+
414+
const spParts = serverPort.split(':');
415+
const port = spParts[1] || 8388;
416+
417+
return {
418+
name: rawName,
419+
config: ` - name: "${safeName}"
420+
type: ss
421+
server: ${spParts[0]}
422+
port: ${port}
423+
cipher: ${method}
424+
password: "${password}"
425+
udp: true`
426+
};
427+
} catch (e) { return null; }
428+
}
361429

362-
// 手动组装逻辑 (简化版,仅作展示)
430+
// --- 激活手动组装逻辑 ---
363431
function addNode() {
364-
alert("手动组装功能暂仅支持基本生成演示,如需完整功能请使用上方批量模式直接导入。");
432+
const nameInput = document.getElementById('customNodeName').value.trim();
433+
const rawName = nameInput || `Node-${manualNodes.length + 1}`;
434+
435+
// 【最终修复】手动模式查重逻辑
436+
let finalName = rawName;
437+
let count = 1;
438+
// 如果名字已存在,自动追加 (1), (2)...
439+
while (manualNodes.some(n => n.name === finalName)) {
440+
finalName = `${rawName} (${count})`;
441+
count++;
442+
}
443+
444+
const safeName = finalName.replace(/"/g, '\\"');
445+
446+
const ip = document.getElementById('ip').value.trim();
447+
const port = document.getElementById('port').value.trim();
448+
const uuid = document.getElementById('uuid').value.trim();
449+
450+
// 【优化】加密方式强制小写,避免兼容性问题
451+
const cipher = document.getElementById('cipher').value.trim().toLowerCase();
452+
const network = document.getElementById('network').value;
453+
const type = document.getElementById('type').value;
454+
455+
if (!ip || !port || !uuid) {
456+
alert("请填写完整的 地址、端口 和 UUID/密码!");
457+
return;
458+
}
459+
460+
let config = "";
461+
if (type === 'vless') {
462+
config = ` - name: "${safeName}"\n type: vless\n server: ${ip}\n port: ${port}\n uuid: ${uuid}\n tls: true\n udp: true\n skip-cert-verify: true\n network: ${network}`;
463+
if(network === 'ws') config += `\n ws-opts:\n path: "/"\n headers:\n Host: ${ip}`;
464+
} else if (type === 'vmess') {
465+
const vmessCipher = cipher || 'auto';
466+
config = ` - name: "${safeName}"\n type: vmess\n server: ${ip}\n port: ${port}\n uuid: ${uuid}\n alterId: 0\n cipher: ${vmessCipher}\n udp: true\n network: ${network}`;
467+
if(network === 'ws') config += `\n ws-opts:\n path: "/"\n headers:\n Host: ${ip}`;
468+
} else if (type === 'ss') {
469+
if(!cipher) { alert("SS 协议必须填写加密方式 (Cipher)!"); return; }
470+
config = ` - name: "${safeName}"\n type: ss\n server: ${ip}\n port: ${port}\n cipher: ${cipher}\n password: "${uuid}"\n udp: true`;
471+
} else if (type === 'hysteria2') {
472+
config = ` - name: "${safeName}"\n type: hysteria2\n server: ${ip}\n port: ${port}\n password: ${uuid}\n skip-cert-verify: true\n up: 100 Mbps\n down: 100 Mbps`;
473+
}
474+
475+
// 保存查重后的名字
476+
manualNodes.push({ name: finalName, config: config });
477+
renderNodeList();
478+
479+
document.getElementById('customNodeName').value = "";
480+
document.getElementById('ip').value = "";
481+
}
482+
483+
function renderNodeList() {
484+
const container = document.getElementById('nodeListContainer');
485+
const list = document.getElementById('nodeList');
486+
list.innerHTML = "";
487+
488+
if (manualNodes.length > 0) {
489+
container.style.display = 'block';
490+
manualNodes.forEach((node, index) => {
491+
const item = document.createElement('div');
492+
item.className = 'node-item';
493+
item.innerHTML = `
494+
<div class="node-info"><strong>${node.name}</strong></div>
495+
<div class="node-delete" onclick="removeNode(${index})">×</div>
496+
`;
497+
list.appendChild(item);
498+
});
499+
} else {
500+
container.style.display = 'none';
501+
}
502+
}
503+
504+
window.removeNode = function(index) {
505+
manualNodes.splice(index, 1);
506+
renderNodeList();
507+
}
508+
509+
function generateFromList() {
510+
if (manualNodes.length === 0) {
511+
alert("还没有添加任何节点!");
512+
return;
513+
}
514+
downloadConfig(manualNodes.map(p => p.config), manualNodes.map(p => p.name));
365515
}
366-
function generateFromList() {}
367516

368517
// --- 模板与下载 ---
369518
const baseTemplate = `
@@ -408,7 +557,8 @@ <h1>汤盛碗的Clash 配置生成器</h1>
408557

409558
function downloadConfig(proxies, names) {
410559
const proxiesStr = proxies.join('\n');
411-
const groupStr = names.map(n => ` - "${n}"`).join('\n');
560+
// Group 列表里的名字也要转义!虽然前面转义过了,但为了保险
561+
const groupStr = names.map(n => ` - "${n.replace(/"/g, '\\"')}"`).join('\n');
412562

413563
const customFileName = document.getElementById('customFileName').value.trim();
414564
const fileName = customFileName ? customFileName : `Clash_Config_${new Date().toISOString().slice(0,10)}`;

0 commit comments

Comments
 (0)