mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-26 20:50:30 +08:00
Compare commits
2 Commits
041a81cfd4
...
51d169d2e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51d169d2e8 | ||
|
|
55416f32cd |
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,3 +10,4 @@ out
|
|||||||
*.ttf
|
*.ttf
|
||||||
party.md
|
party.md
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
tsconfig.node.tsbuildinfo
|
||||||
|
|||||||
@ -129,6 +129,60 @@ export async function getCurrentProfileItem(): Promise<IProfileItem> {
|
|||||||
return (await getProfileItem(current)) || { id: 'default', type: 'local', name: '空白订阅' }
|
return (await getProfileItem(current)) || { id: 'default', type: 'local', name: '空白订阅' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FetchOptions {
|
||||||
|
url: string
|
||||||
|
useProxy: boolean
|
||||||
|
mixedPort: number
|
||||||
|
userAgent: string
|
||||||
|
authToken?: string
|
||||||
|
timeout: number
|
||||||
|
substore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchResult {
|
||||||
|
data: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAndValidateSubscription(options: FetchOptions): Promise<FetchResult> {
|
||||||
|
const { url, useProxy, mixedPort, userAgent, authToken, timeout, substore } = options
|
||||||
|
|
||||||
|
const headers: Record<string, string> = { 'User-Agent': userAgent }
|
||||||
|
if (authToken) headers['Authorization'] = authToken
|
||||||
|
|
||||||
|
let res: chromeRequest.Response<string>
|
||||||
|
if (substore) {
|
||||||
|
const urlObj = new URL(`http://127.0.0.1:${subStorePort}${url}`)
|
||||||
|
urlObj.searchParams.set('target', 'ClashMeta')
|
||||||
|
urlObj.searchParams.set('noCache', 'true')
|
||||||
|
if (useProxy) {
|
||||||
|
urlObj.searchParams.set('proxy', `http://127.0.0.1:${mixedPort}`)
|
||||||
|
}
|
||||||
|
res = await chromeRequest.get(urlObj.toString(), { headers, responseType: 'text', timeout })
|
||||||
|
} else {
|
||||||
|
res = await chromeRequest.get(url, {
|
||||||
|
headers,
|
||||||
|
responseType: 'text',
|
||||||
|
timeout,
|
||||||
|
proxy: useProxy ? { protocol: 'http', host: '127.0.0.1', port: mixedPort } : false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status < 200 || res.status >= 300) {
|
||||||
|
throw new Error(`Subscription failed: Request status code ${res.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parse(res.data) as Record<string, unknown> | null
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
|
throw new Error('Subscription failed: Profile is not a valid YAML')
|
||||||
|
}
|
||||||
|
if (!parsed['proxies'] && !parsed['proxy-providers']) {
|
||||||
|
throw new Error('Subscription failed: Profile missing proxies or providers')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: res.data, headers: res.headers }
|
||||||
|
}
|
||||||
|
|
||||||
export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> {
|
export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> {
|
||||||
const id = item.id || new Date().getTime().toString(16)
|
const id = item.id || new Date().getTime().toString(16)
|
||||||
const newItem = {
|
const newItem = {
|
||||||
@ -145,71 +199,54 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
authToken: item.authToken,
|
authToken: item.authToken,
|
||||||
updated: new Date().getTime()
|
updated: new Date().getTime()
|
||||||
} as IProfileItem
|
} as IProfileItem
|
||||||
|
|
||||||
switch (newItem.type) {
|
switch (newItem.type) {
|
||||||
case 'remote': {
|
case 'remote': {
|
||||||
const { userAgent, subscriptionTimeout = 30000 } = await getAppConfig()
|
const { userAgent, subscriptionTimeout = 30000 } = await getAppConfig()
|
||||||
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
|
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
|
||||||
if (!item.url) throw new Error('Empty URL')
|
if (!item.url) throw new Error('Empty URL')
|
||||||
let res: chromeRequest.Response<string>
|
|
||||||
if (newItem.substore) {
|
const baseOptions: Omit<FetchOptions, 'useProxy' | 'timeout'> = {
|
||||||
const urlObj = new URL(`http://127.0.0.1:${subStorePort}${item.url}`)
|
url: item.url,
|
||||||
urlObj.searchParams.set('target', 'ClashMeta')
|
mixedPort,
|
||||||
urlObj.searchParams.set('noCache', 'true')
|
userAgent: userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`,
|
||||||
|
authToken: item.authToken,
|
||||||
|
substore: newItem.substore || false
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: FetchResult
|
||||||
|
let finalUseProxy = newItem.useProxy
|
||||||
|
|
||||||
if (newItem.useProxy) {
|
if (newItem.useProxy) {
|
||||||
urlObj.searchParams.set('proxy', `http://127.0.0.1:${mixedPort}`)
|
result = await fetchAndValidateSubscription({
|
||||||
} else {
|
...baseOptions,
|
||||||
urlObj.searchParams.delete('proxy')
|
useProxy: true,
|
||||||
}
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
|
|
||||||
}
|
|
||||||
if (item.authToken) {
|
|
||||||
headers['Authorization'] = item.authToken
|
|
||||||
}
|
|
||||||
res = await chromeRequest.get(urlObj.toString(), {
|
|
||||||
headers,
|
|
||||||
responseType: 'text',
|
|
||||||
timeout: subscriptionTimeout
|
timeout: subscriptionTimeout
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const headers: Record<string, string> = {
|
const smartTimeout = 5000
|
||||||
'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
|
try {
|
||||||
}
|
result = await fetchAndValidateSubscription({
|
||||||
if (item.authToken) {
|
...baseOptions,
|
||||||
headers['Authorization'] = item.authToken
|
useProxy: false,
|
||||||
}
|
timeout: smartTimeout
|
||||||
res = await chromeRequest.get(item.url, {
|
|
||||||
proxy: newItem.useProxy
|
|
||||||
? {
|
|
||||||
protocol: 'http',
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: mixedPort
|
|
||||||
}
|
|
||||||
: false,
|
|
||||||
headers,
|
|
||||||
responseType: 'text',
|
|
||||||
timeout: subscriptionTimeout
|
|
||||||
})
|
})
|
||||||
|
} catch (directError) {
|
||||||
|
try {
|
||||||
|
result = await fetchAndValidateSubscription({
|
||||||
|
...baseOptions,
|
||||||
|
useProxy: true,
|
||||||
|
timeout: smartTimeout
|
||||||
|
})
|
||||||
|
finalUseProxy = true
|
||||||
|
} catch {
|
||||||
|
throw directError
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查状态码,例如:403
|
newItem.useProxy = finalUseProxy
|
||||||
if (res.status < 200 || res.status >= 300) {
|
const { data, headers } = result
|
||||||
throw new Error(`Subscription failed: Request status code ${res.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = res.data
|
|
||||||
const headers = res.headers
|
|
||||||
|
|
||||||
// 校验是否为对象结构 (拦截 HTML字符串、普通文本、乱码)
|
|
||||||
const parsed = parse(data)
|
|
||||||
if (typeof parsed !== 'object' || parsed === null) {
|
|
||||||
throw new Error('Subscription failed: Profile is not a valid YAML')
|
|
||||||
}
|
|
||||||
// 检查是否包含必要的字段,防止空对象
|
|
||||||
const profile = parsed as any
|
|
||||||
if (!profile.proxies && !profile['proxy-providers']) {
|
|
||||||
throw new Error('Subscription failed: Profile missing proxies or providers')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (headers['content-disposition'] && newItem.name === 'Remote File') {
|
if (headers['content-disposition'] && newItem.name === 'Remote File') {
|
||||||
newItem.name = parseFilename(headers['content-disposition'])
|
newItem.name = parseFilename(headers['content-disposition'])
|
||||||
@ -217,11 +254,9 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
if (headers['profile-web-page-url']) {
|
if (headers['profile-web-page-url']) {
|
||||||
newItem.home = headers['profile-web-page-url']
|
newItem.home = headers['profile-web-page-url']
|
||||||
}
|
}
|
||||||
if (headers['profile-update-interval']) {
|
if (headers['profile-update-interval'] && !item.allowFixedInterval) {
|
||||||
if (!item.allowFixedInterval) {
|
|
||||||
newItem.interval = parseInt(headers['profile-update-interval']) * 60
|
newItem.interval = parseInt(headers['profile-update-interval']) * 60
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (headers['subscription-userinfo']) {
|
if (headers['subscription-userinfo']) {
|
||||||
newItem.extra = parseSubinfo(headers['subscription-userinfo'])
|
newItem.extra = parseSubinfo(headers['subscription-userinfo'])
|
||||||
}
|
}
|
||||||
@ -229,8 +264,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'local': {
|
case 'local': {
|
||||||
const data = item.file || ''
|
await setProfileStr(id, item.file || '')
|
||||||
await setProfileStr(id, data)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -272,9 +306,28 @@ export async function setProfileStr(id: string, content: string): Promise<void>
|
|||||||
|
|
||||||
export async function getProfile(id: string | undefined): Promise<IMihomoConfig> {
|
export async function getProfile(id: string | undefined): Promise<IMihomoConfig> {
|
||||||
const profile = await getProfileStr(id)
|
const profile = await getProfileStr(id)
|
||||||
|
|
||||||
|
// 检测是否为 HTML 内容(订阅返回错误页面)
|
||||||
|
const trimmed = profile.trim()
|
||||||
|
if (
|
||||||
|
trimmed.startsWith('<!DOCTYPE') ||
|
||||||
|
trimmed.startsWith('<html') ||
|
||||||
|
trimmed.startsWith('<HTML') ||
|
||||||
|
/<style[^>]*>/i.test(trimmed.slice(0, 500))
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Profile "${id}" contains HTML instead of YAML. The subscription may have returned an error page. Please re-import or update the subscription.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
let result = parse(profile)
|
let result = parse(profile)
|
||||||
if (typeof result !== 'object') result = {}
|
if (typeof result !== 'object') result = {}
|
||||||
return result as IMihomoConfig
|
return result as IMihomoConfig
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
|
throw new Error(`Failed to parse profile "${id}": ${msg}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// attachment;filename=xxx.yaml; filename*=UTF-8''%xx%xx%xx
|
// attachment;filename=xxx.yaml; filename*=UTF-8''%xx%xx%xx
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user