feat:移除了弹窗,服务器添加sls

This commit is contained in:
AxTps committed 2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 files changed
+1322534

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
import type { RequestOptions } from '../types/request'
export function appendRequestParams(url: string, params?: RequestOptions['params']): string {
if (!params) return url
const filteredParams: Record<string, string> = {}
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
filteredParams[key] = String(value)
}
}
const queryString = new URLSearchParams(filteredParams).toString()
if (!queryString) return url
return `${url}${url.includes('?') ? '&' : '?'}${queryString}`
}
export function toFetchBody(body: unknown): BodyInit | null | undefined {
if (!body) return undefined
if (
typeof body === 'object' &&
!(body instanceof FormData) &&
!(body instanceof URLSearchParams) &&
!(body instanceof Blob) &&
!(body instanceof ArrayBuffer) &&
!ArrayBuffer.isView(body as ArrayBufferView)
) {
return JSON.stringify(body)
}
return body as BodyInit
}
export async function parseResponseErrorData(response: Response): Promise<unknown> {
const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''
try {
if (contentType.includes('application/json') || contentType.includes('+json')) {
return await response.json()
}
const text = await response.text()
if (!text) return undefined
try {
return JSON.parse(text)
} catch {
return text
}
} catch {
return undefined
}
}
@@ -0,0 +1,20 @@
import { ModrinthApiError } from '../core/errors'
/**
* Wrap a function with JWT retry logic.
* On 401, calls refreshToken() and retries once.
*/
export async function withJWTRetry<T>(
fn: () => Promise<T>,
refreshToken: () => Promise<void>,
): Promise<T> {
try {
return await fn()
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 401) {
await refreshToken()
return await fn()
}
throw error
}
}
+18
View File
@@ -0,0 +1,18 @@
const NODE_FS_PATH_REGEX = /\/modrinth\/v\d+\/fs\/?$/
const HTTP_SCHEME_REGEX = /^https?:\/\//i
const WS_SCHEME_REGEX = /^wss?:\/\//i
const HTTP_SECURE_SCHEME_REGEX = /^https:\/\//i
const HTTP_INSECURE_SCHEME_REGEX = /^http:\/\//i
export function getNodeBaseUrl(url: string): string {
const baseUrl = url.replace(NODE_FS_PATH_REGEX, '')
return HTTP_SCHEME_REGEX.test(baseUrl) ? baseUrl : `https://${baseUrl}`
}
export function getNodeWebSocketUrl(url: string): string {
if (WS_SCHEME_REGEX.test(url)) return url
if (HTTP_SECURE_SCHEME_REGEX.test(url)) return url.replace(HTTP_SECURE_SCHEME_REGEX, 'wss://')
if (HTTP_INSECURE_SCHEME_REGEX.test(url)) return url.replace(HTTP_INSECURE_SCHEME_REGEX, 'ws://')
return `wss://${url}`
}
+97
View File
@@ -0,0 +1,97 @@
export interface WebSocketPingOptions {
count?: number
intervalMs?: number
settleDelayMs?: number
timeoutMs?: number
signal?: AbortSignal
}
export async function pingWebSocketUrl(
url: string,
options: WebSocketPingOptions = {},
): Promise<number> {
const count = options.count ?? 5
const intervalMs = options.intervalMs ?? 200
const settleDelayMs = options.settleDelayMs ?? 1000
const timeoutMs = options.timeoutMs ?? count * intervalMs + settleDelayMs + 1000
if (options.signal?.aborted) return -1
return await new Promise<number>((resolve) => {
const samples: number[] = []
const timers = new Set<ReturnType<typeof setTimeout>>()
let socket: WebSocket | undefined
let settled = false
const setTrackedTimeout = (callback: () => void, ms: number) => {
const timer = setTimeout(() => {
timers.delete(timer)
callback()
}, ms)
timers.add(timer)
return timer
}
const cleanup = () => {
for (const timer of timers) clearTimeout(timer)
timers.clear()
options.signal?.removeEventListener('abort', abort)
if (
socket &&
(socket.readyState === WebSocket.CONNECTING || socket.readyState === WebSocket.OPEN)
) {
socket.close()
}
}
const finish = (ping: number) => {
if (settled) return
settled = true
cleanup()
resolve(ping)
}
const abort = () => finish(-1)
options.signal?.addEventListener('abort', abort, { once: true })
try {
socket = new WebSocket(url)
} catch {
finish(-1)
return
}
setTrackedTimeout(() => finish(-1), timeoutMs)
socket.onopen = () => {
for (let i = 0; i < count; i++) {
setTrackedTimeout(() => {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(String(performance.now()))
}
}, i * intervalMs)
}
setTrackedTimeout(
() => {
const ping =
samples.length > 0
? Math.round([...samples].sort((a, b) => a - b)[Math.floor(samples.length / 2)])
: -1
finish(ping)
},
count * intervalMs + settleDelayMs,
)
}
socket.onmessage = (event) => {
samples.push(performance.now() - Number(event.data))
}
socket.onerror = () => finish(-1)
socket.onclose = () => {
if (!settled && samples.length === 0) finish(-1)
}
})
}
+139
View File
@@ -0,0 +1,139 @@
import type { Archon } from '../modules/archon/types'
export type ParsedSseEvent = {
kind: 'event'
id?: string
event?: string
data: string
}
export type ParsedSseRetry = {
kind: 'retry'
retry: number
}
export type ParsedSseItem = ParsedSseEvent | ParsedSseRetry
export class SseParser {
private buffer = ''
private eventName = ''
private data = ''
private id: string | undefined
feed(chunk: string): ParsedSseItem[] {
this.buffer += chunk
const items: ParsedSseItem[] = []
while (true) {
const lineEnd = this.findLineEnd()
if (!lineEnd) break
const { line, length } = lineEnd
this.buffer = this.buffer.slice(length)
this.processLine(line, items)
}
return items
}
end(): ParsedSseItem[] {
const items: ParsedSseItem[] = []
if (this.buffer.length > 0) {
this.processLine(this.buffer.endsWith('\r') ? this.buffer.slice(0, -1) : this.buffer, items)
this.buffer = ''
}
this.dispatch(items)
return items
}
private findLineEnd(): { line: string; length: number } | null {
const lf = this.buffer.indexOf('\n')
const cr = this.buffer.indexOf('\r')
if (lf === -1 && cr === -1) return null
if (cr !== -1 && (lf === -1 || cr < lf)) {
if (cr === this.buffer.length - 1) return null
const length = this.buffer[cr + 1] === '\n' ? cr + 2 : cr + 1
return {
line: this.buffer.slice(0, cr),
length,
}
}
return {
line: this.buffer.slice(0, lf),
length: lf + 1,
}
}
private processLine(line: string, items: ParsedSseItem[]): void {
if (line === '') {
this.dispatch(items)
return
}
if (line.startsWith(':')) return
const colon = line.indexOf(':')
const field = colon === -1 ? line : line.slice(0, colon)
let value = colon === -1 ? '' : line.slice(colon + 1)
if (value.startsWith(' ')) value = value.slice(1)
switch (field) {
case 'event':
this.eventName = value
break
case 'data':
this.data += `${value}\n`
break
case 'id':
this.id = value
break
case 'retry': {
const retry = Number(value)
if (Number.isInteger(retry) && retry >= 0) {
items.push({ kind: 'retry', retry })
}
break
}
}
}
private dispatch(items: ParsedSseItem[]): void {
if (!this.data) {
this.eventName = ''
this.id = undefined
return
}
items.push({
kind: 'event',
id: this.id,
event: this.eventName || undefined,
data: this.data.endsWith('\n') ? this.data.slice(0, -1) : this.data,
})
this.eventName = ''
this.data = ''
this.id = undefined
}
}
export function parseSyncEventData(data: string): Archon.Sync.v1.SyncEvent | null {
let parsed: unknown
try {
parsed = JSON.parse(data)
} catch {
return null
}
if (!parsed || typeof parsed !== 'object') return null
const event = parsed as { type?: unknown }
if (typeof event.type !== 'string') return null
return parsed as Archon.Sync.v1.SyncEvent
}
+2
View File
@@ -0,0 +1,2 @@
export type Override<T, R> = Omit<T, keyof R> & R
export type RawDecimal = string | number