Skip to content

Commit 2e98b45

Browse files
committed
fix: 添加Clash订阅配置文件热补丁,支持UUID和ECH配置,优化Singbox处理逻辑
1 parent 8350ac1 commit 2e98b45

1 file changed

Lines changed: 139 additions & 2 deletions

File tree

_worker.js

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ export default {
324324
订阅内容 = Singbox订阅配置文件热补丁(订阅内容, config_JSON.UUID, config_JSON.Fingerprint, config_JSON.ECH ? await getECH(host) : null);
325325
responseHeaders["content-type"] = 'application/json; charset=utf-8';
326326
} else if (订阅类型 === 'clash') {
327+
订阅内容 = Clash订阅配置文件热补丁(订阅内容, config_JSON.UUID, config_JSON.ECH ? await getECH(host) : null);
327328
responseHeaders["content-type"] = 'application/x-yaml; charset=utf-8';
328329
}
329330
return new Response(订阅内容, { status: 200, headers: responseHeaders });
@@ -784,6 +785,142 @@ async function httpConnect(targetHost, targetPort, initialData) {
784785
}
785786
}
786787
//////////////////////////////////////////////////功能性函数///////////////////////////////////////////////
788+
function Clash订阅配置文件热补丁(clash_yaml, uuid = null, ech_config = null) {
789+
// 判断uuid字段是否为真
790+
if (!uuid) {
791+
return clash_yaml;
792+
}
793+
794+
// 如果ech_config为null,直接返回
795+
if (!ech_config) return clash_yaml;
796+
797+
const lines = clash_yaml.split('\n');
798+
const processedLines = [];
799+
let i = 0;
800+
801+
while (i < lines.length) {
802+
const line = lines[i];
803+
const trimmedLine = line.trim();
804+
805+
// 处理行格式(Flow):- {name: ..., uuid: ..., ...}
806+
if (trimmedLine.startsWith('- {') && (trimmedLine.includes('uuid:') || trimmedLine.includes('password:'))) {
807+
let fullNode = line;
808+
let braceCount = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
809+
810+
// 如果括号不匹配,继续读取下一行
811+
while (braceCount > 0 && i + 1 < lines.length) {
812+
i++;
813+
fullNode += '\n' + lines[i];
814+
braceCount += (lines[i].match(/\{/g) || []).length - (lines[i].match(/\}/g) || []).length;
815+
}
816+
817+
// 获取代理类型
818+
const typeMatch = fullNode.match(/type:\s*(\w+)/);
819+
const proxyType = typeMatch ? typeMatch[1] : 'vless';
820+
821+
// 根据代理类型确定要查找的字段
822+
let credentialField = 'uuid';
823+
if (proxyType === 'trojan') {
824+
credentialField = 'password';
825+
}
826+
827+
// 检查对应字段的值是否匹配
828+
const credentialPattern = new RegExp(`${credentialField}:\\s*([^,}\\n]+)`);
829+
const credentialMatch = fullNode.match(credentialPattern);
830+
831+
if (credentialMatch && credentialMatch[1].trim() === uuid.trim()) {
832+
// 在最后一个}前添加ech-opts
833+
fullNode = fullNode.replace(/\}(\s*)$/, `, ech-opts: {enable: true, config: "${ech_config}"}}$1`);
834+
}
835+
836+
processedLines.push(fullNode);
837+
i++;
838+
}
839+
// 处理块格式(Block):- name: ..., 后续行为属性
840+
else if (trimmedLine.startsWith('- name:')) {
841+
// 收集完整的代理节点定义
842+
let nodeLines = [line];
843+
let baseIndent = line.search(/\S/);
844+
let topLevelIndent = baseIndent + 2; // 顶级属性的缩进
845+
i++;
846+
847+
// 继续读取这个节点的所有属性
848+
while (i < lines.length) {
849+
const nextLine = lines[i];
850+
const nextTrimmed = nextLine.trim();
851+
852+
// 如果是空行,包含它但不继续
853+
if (!nextTrimmed) {
854+
nodeLines.push(nextLine);
855+
i++;
856+
break;
857+
}
858+
859+
const nextIndent = nextLine.search(/\S/);
860+
861+
// 如果缩进小于等于基础缩进且不是空行,说明节点结束了
862+
if (nextIndent <= baseIndent && nextTrimmed.startsWith('- ')) {
863+
break;
864+
}
865+
866+
// 如果缩进更小,节点也结束了
867+
if (nextIndent < baseIndent && nextTrimmed) {
868+
break;
869+
}
870+
871+
nodeLines.push(nextLine);
872+
i++;
873+
}
874+
875+
// 获取代理类型
876+
const nodeText = nodeLines.join('\n');
877+
const typeMatch = nodeText.match(/type:\s*(\w+)/);
878+
const proxyType = typeMatch ? typeMatch[1] : 'vless';
879+
880+
// 根据代理类型确定要查找的字段
881+
let credentialField = 'uuid';
882+
if (proxyType === 'trojan') {
883+
credentialField = 'password';
884+
}
885+
886+
// 检查这个节点的对应字段是否匹配
887+
const credentialPattern = new RegExp(`${credentialField}:\\s*([^\\n]+)`);
888+
const credentialMatch = nodeText.match(credentialPattern);
889+
890+
if (credentialMatch && credentialMatch[1].trim() === uuid.trim()) {
891+
// 找到在哪里插入ech-opts
892+
// 策略:在最后一个顶级属性后面插入,或在ws-opts之前插入
893+
let insertIndex = -1;
894+
895+
for (let j = nodeLines.length - 1; j >= 0; j--) {
896+
// 跳过空行,找到节点中最后一个非空行(可能是顶级属性或其子项)
897+
if (nodeLines[j].trim()) {
898+
insertIndex = j;
899+
break;
900+
}
901+
}
902+
903+
if (insertIndex >= 0) {
904+
const indent = ' '.repeat(topLevelIndent);
905+
// 在节点末尾(最后一个属性块之后)插入 ech-opts 属性
906+
nodeLines.splice(insertIndex + 1, 0,
907+
`${indent}ech-opts:`,
908+
`${indent} enable: true`,
909+
`${indent} config: "${ech_config}"`
910+
);
911+
}
912+
}
913+
914+
processedLines.push(...nodeLines);
915+
} else {
916+
processedLines.push(line);
917+
i++;
918+
}
919+
}
920+
921+
return processedLines.join('\n');
922+
}
923+
787924
function Singbox订阅配置文件热补丁(sb_json_text, uuid = null, fingerprint = "chrome", ech_config = null) {
788925
try {
789926
let config = JSON.parse(sb_json_text);
@@ -932,8 +1069,8 @@ function Singbox订阅配置文件热补丁(sb_json_text, uuid = null, fingerpri
9321069
// --- 4. UUID 匹配节点的 TLS 热补丁 (utls & ech) ---
9331070
if (uuid) {
9341071
config.outbounds.forEach(outbound => {
935-
// 仅处理包含 uuid 且匹配的节点
936-
if (outbound.uuid && outbound.uuid === uuid) {
1072+
// 仅处理包含 uuid 或 password 且匹配的节点
1073+
if ((outbound.uuid && outbound.uuid === uuid) || (outbound.password && outbound.password === uuid)) {
9371074
// 确保 tls 对象存在
9381075
if (!outbound.tls) {
9391076
outbound.tls = { enabled: true };

0 commit comments

Comments
 (0)