From 063c6b1f5a82ace8bcb2a8abd9a4b8dcba9c15dc Mon Sep 17 00:00:00 2001 From: letdan <47627026+lostornot@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:50:01 +0800 Subject: [PATCH 1/2] Add ChatGPT Voice IP rules --- Build/build-openai-voice.ts | 93 +++++++++++++++++++++++++++++++++++++ Build/index.ts | 2 + 2 files changed, 95 insertions(+) create mode 100644 Build/build-openai-voice.ts diff --git a/Build/build-openai-voice.ts b/Build/build-openai-voice.ts new file mode 100644 index 0000000000..4e0fe9c5fc --- /dev/null +++ b/Build/build-openai-voice.ts @@ -0,0 +1,93 @@ +import { fastIpVersion } from 'foxts/fast-ip-version'; +import { SHARED_DESCRIPTION } from './constants/description'; +import { $$fetch } from './lib/fetch-retry'; +import { RulesetOutput } from './lib/rules/ruleset'; +import { task } from './trace'; + +const OPENAI_VOICE_IP_URL = 'https://openai.com/chatgpt-voice.json'; + +function readPrefix( + entry: Record, + field: 'ipv4Prefix' | 'ipv6Prefix', + version: 4 | 6, + index: number +) { + const value = entry[field]; + if (value === undefined) return null; + + if (typeof value !== 'string' || fastIpVersion(value) !== version) { + throw new TypeError(`Invalid ${field} at prefixes[${index}]`); + } + + return value; +} + +function parseOpenAIVoiceJSON(data: unknown) { + if ( + data === null + || typeof data !== 'object' + || !('prefixes' in data) + || !Array.isArray(data.prefixes) + ) { + throw new TypeError('Invalid OpenAI Voice IP list: missing prefixes array'); + } + + if (!('creationTime' in data) || typeof data.creationTime !== 'string') { + throw new TypeError('Invalid OpenAI Voice IP list: missing creationTime'); + } + + const lastUpdated = new Date(data.creationTime); + if (Number.isNaN(lastUpdated.getTime())) { + throw new TypeError('Invalid OpenAI Voice IP list: invalid creationTime'); + } + + const cidr4 = new Set(); + const cidr6 = new Set(); + + for (let i = 0, len = data.prefixes.length; i < len; i++) { + const entry = data.prefixes[i]; + if (entry === null || typeof entry !== 'object') { + throw new TypeError(`Invalid OpenAI Voice IP list entry at prefixes[${i}]`); + } + + const ipv4 = readPrefix(entry as Record, 'ipv4Prefix', 4, i); + const ipv6 = readPrefix(entry as Record, 'ipv6Prefix', 6, i); + + if (ipv4 === null && ipv6 === null) { + throw new TypeError(`OpenAI Voice IP list entry prefixes[${i}] has no IP prefix`); + } + + if (ipv4 !== null) cidr4.add(ipv4); + if (ipv6 !== null) cidr6.add(ipv6); + } + + if (cidr4.size + cidr6.size === 0) { + throw new TypeError('OpenAI Voice IP list is empty'); + } + + return { + cidr4: Array.from(cidr4), + cidr6: Array.from(cidr6), + lastUpdated + }; +} + +export const buildOpenAIVoice = task(require.main === module, __filename)(async (span) => { + const { cidr4, cidr6, lastUpdated } = await span.traceChildAsync('get OpenAI Voice IP ranges', async () => { + const response = await $$fetch(OPENAI_VOICE_IP_URL); + return parseOpenAIVoiceJSON(await response.json()); + }); + + return new RulesetOutput(span, 'ai', 'ip') + .withTitle('Sukka\'s Ruleset - ChatGPT Voice IP CIDR') + .appendDescription( + SHARED_DESCRIPTION, + '', + 'This file contains IP ranges used by ChatGPT Voice.' + ) + .withDate(lastUpdated) + .appendDataSource(`${OPENAI_VOICE_IP_URL} (last updated: ${lastUpdated.toISOString()})`) + .bulkAddCIDR4NoResolve(cidr4) + .bulkAddCIDR6NoResolve(cidr6) + .write(); +}); diff --git a/Build/index.ts b/Build/index.ts index e75f13f110..2c2e687ae3 100644 --- a/Build/index.ts +++ b/Build/index.ts @@ -6,6 +6,7 @@ import { downloadPreviousBuild } from './download-previous-build'; import { buildCommon } from './build-common'; import { buildRejectIPList } from './build-reject-ip-list'; import { buildAppleCdn } from './build-apple-cdn'; +import { buildOpenAIVoice } from './build-openai-voice'; import { buildRejectDomainSet } from './build-reject-domainset'; import { buildChnCidr } from './build-chn-cidr'; import { buildSpeedtestDomainSet } from './build-speedtest-domainset'; @@ -103,6 +104,7 @@ const buildFinishedLock = path.join(ROOT_DIR, '.BUILD_FINISHED'); downloadPreviousBuildPromise.then(() => buildCommon()), downloadPreviousBuildPromise.then(() => buildRejectIPList()), downloadPreviousBuildPromise.then(() => buildAppleCdn()), + downloadPreviousBuildPromise.then(() => buildOpenAIVoice()), downloadPreviousBuildPromise.then(() => cdnDownloadWorker.buildCdnDownloadConf()), downloadPreviousBuildPromise.then(() => buildRejectDomainSet()), downloadPreviousBuildPromise.then(() => telegramCidrWorker.buildTelegramCIDR()), From 68cbe292c837fb1d5e7e7e6530a691fc9e1d0af2 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 2 Aug 2026 16:31:31 +0800 Subject: [PATCH 2/2] Refactor: Build AI CIDR --- ...build-openai-voice.ts => build-ai-cidr.ts} | 49 ++++++++----------- Build/index.ts | 4 +- README.md | 17 ++++++- 3 files changed, 38 insertions(+), 32 deletions(-) rename Build/{build-openai-voice.ts => build-ai-cidr.ts} (66%) diff --git a/Build/build-openai-voice.ts b/Build/build-ai-cidr.ts similarity index 66% rename from Build/build-openai-voice.ts rename to Build/build-ai-cidr.ts index 4e0fe9c5fc..d9031b771e 100644 --- a/Build/build-openai-voice.ts +++ b/Build/build-ai-cidr.ts @@ -6,22 +6,6 @@ import { task } from './trace'; const OPENAI_VOICE_IP_URL = 'https://openai.com/chatgpt-voice.json'; -function readPrefix( - entry: Record, - field: 'ipv4Prefix' | 'ipv6Prefix', - version: 4 | 6, - index: number -) { - const value = entry[field]; - if (value === undefined) return null; - - if (typeof value !== 'string' || fastIpVersion(value) !== version) { - throw new TypeError(`Invalid ${field} at prefixes[${index}]`); - } - - return value; -} - function parseOpenAIVoiceJSON(data: unknown) { if ( data === null @@ -46,23 +30,30 @@ function parseOpenAIVoiceJSON(data: unknown) { for (let i = 0, len = data.prefixes.length; i < len; i++) { const entry = data.prefixes[i]; - if (entry === null || typeof entry !== 'object') { + if (typeof entry !== 'object' || entry == null) { throw new TypeError(`Invalid OpenAI Voice IP list entry at prefixes[${i}]`); } - const ipv4 = readPrefix(entry as Record, 'ipv4Prefix', 4, i); - const ipv6 = readPrefix(entry as Record, 'ipv6Prefix', 6, i); - - if (ipv4 === null && ipv6 === null) { - throw new TypeError(`OpenAI Voice IP list entry prefixes[${i}] has no IP prefix`); + const prefixes = Object.values(entry); + if (prefixes.length === 0) { + continue; } - if (ipv4 !== null) cidr4.add(ipv4); - if (ipv6 !== null) cidr6.add(ipv6); - } - - if (cidr4.size + cidr6.size === 0) { - throw new TypeError('OpenAI Voice IP list is empty'); + for (let j = 0, prefixLen = prefixes.length; j < prefixLen; j++) { + const prefix = prefixes[j]; + if (typeof prefix !== 'string') { + throw new TypeError(`Invalid IP prefix at prefixes[${i}]`); + } + + const version = fastIpVersion(prefix); + if (version === 4) { + cidr4.add(prefix); + } else if (version === 6) { + cidr6.add(prefix); + } else { + throw new TypeError(`Invalid IP prefix at prefixes[${i}]`); + } + } } return { @@ -72,7 +63,7 @@ function parseOpenAIVoiceJSON(data: unknown) { }; } -export const buildOpenAIVoice = task(require.main === module, __filename)(async (span) => { +export const buildAICIDR = task(require.main === module, __filename)(async (span) => { const { cidr4, cidr6, lastUpdated } = await span.traceChildAsync('get OpenAI Voice IP ranges', async () => { const response = await $$fetch(OPENAI_VOICE_IP_URL); return parseOpenAIVoiceJSON(await response.json()); diff --git a/Build/index.ts b/Build/index.ts index 2c2e687ae3..e03d7be120 100644 --- a/Build/index.ts +++ b/Build/index.ts @@ -6,7 +6,7 @@ import { downloadPreviousBuild } from './download-previous-build'; import { buildCommon } from './build-common'; import { buildRejectIPList } from './build-reject-ip-list'; import { buildAppleCdn } from './build-apple-cdn'; -import { buildOpenAIVoice } from './build-openai-voice'; +import { buildAICIDR } from './build-ai-cidr'; import { buildRejectDomainSet } from './build-reject-domainset'; import { buildChnCidr } from './build-chn-cidr'; import { buildSpeedtestDomainSet } from './build-speedtest-domainset'; @@ -104,7 +104,7 @@ const buildFinishedLock = path.join(ROOT_DIR, '.BUILD_FINISHED'); downloadPreviousBuildPromise.then(() => buildCommon()), downloadPreviousBuildPromise.then(() => buildRejectIPList()), downloadPreviousBuildPromise.then(() => buildAppleCdn()), - downloadPreviousBuildPromise.then(() => buildOpenAIVoice()), + downloadPreviousBuildPromise.then(() => buildAICIDR()), downloadPreviousBuildPromise.then(() => cdnDownloadWorker.buildCdnDownloadConf()), downloadPreviousBuildPromise.then(() => buildRejectDomainSet()), downloadPreviousBuildPromise.then(() => telegramCidrWorker.buildTelegramCIDR()), diff --git a/README.md b/README.md index 9f1393d440..02216c308b 100644 --- a/README.md +++ b/README.md @@ -423,16 +423,22 @@ rules: #### AI -- 域名规则 人工维护 +- 域名和 IP 规则,人工维护 + 自动更新 - 包含 OpenAI、Google Gemini、Claude、Perplexity 等 **Surge** ```ini +# Non IP RULE-SET,https://ruleset.skk.moe/List/non_ip/ai.conf,[Replace with your policy] RULE-SET,https://ruleset.skk.moe/List/non_ip/apple_intelligence.conf,[Replace with your policy],extended-matching ``` +```ini +# IP +RULE-SET,https://ruleset.skk.moe/List/ip/ai.conf,[Replace with your policy] +``` + **Mihomo** ```yaml @@ -451,10 +457,19 @@ rule-providers: interval: 43200 url: https://ruleset.skk.moe/Clash/non_ip/apple_intelligence.txt path: ./sukkaw_ruleset/apple_intelligence_non_ip.txt + ai_ip: + type: http + behavior: classical + format: text + interval: 43200 + url: https://ruleset.skk.moe/Clash/ip/ai.txt + path: ./sukkaw_ruleset/ai_ip.txt rules: - RULE-SET,ai_non_ip,[Replace with your policy] - RULE-SET,apple_intelligence_non_ip,[Replace with your policy] + + - RULE-SET,ai_ip,[Replace with your policy] ``` #### Telegram