1+
12<!DOCTYPE html>
23< html lang ="zh-CN ">
34< head >
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 ( / n a m e : \s * [ " ' ] .* ?[ " ' ] / , `name: "${ finalName } "` ) ;
220+
221+ // 即使重名,替换时也要注意转义
222+ const safeName = finalName . replace ( / " / g, '\\"' ) ;
223+ result . config = result . config . replace ( / n a m e : \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