feat: rn-update 进度回调 + rn-xwebview JSBridge + rn-log v0.1.0 新建

Agent 3 — rn-update:
- downloadPlugin/downloadApk 新增 onProgress 进度回调
- checkAppUpdate/checkPluginUpdate 版本缓存(30分钟 TTL)
- 新增 UpdateDownloadProgress 类型导出

Agent 3 — rn-xwebview:
- XWebViewBridge 补全标准 JSBridge handler
- getDeviceInfo/getToken/getUserInfo/openNativePage/closeWebView/showToast

Agent 4 — rn-log v0.1.0:
- XLog 主入口:event/captureError/warn/info/startCapture
- LogQueue:AsyncStorage 本地队列 + 批量上报
- ErrorCapture:JS global error + unhandledRejection
- FunnelTracker:漏斗分析
- fingerprint:SHA-256 指纹去重
- HttpInterceptor:rn-common HTTP 错误自动上报
- NativeLogReporter:TurboModule spec
这个提交包含在:
XuqmGroup 2026-06-16 12:10:28 +08:00
父节点 97d4d9498a
当前提交 16750b0421
共有 23 个文件被更改,包括 992 次插入8 次删除

查看文件

@ -37,6 +37,8 @@
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-native": "^0.73.0",
"typescript": "^5.9.3"
"axios": "^1.18.0",
"typescript": "^5.9.3",
"zod": "3.23.8"
}
}

查看文件

@ -1,7 +1,9 @@
export { useRequest } from './useRequest'
export { useApi } from './useApi'
export { usePageApi } from './usePageApi'
export { RequestError } from './errors'
export { setGlobalApiErrorHandler } from './globalErrorHandler'
export type { RequestOptions } from './useRequest'
export type { ApiMethod } from './useApi'
export type { PageOptions } from './usePageApi'
export type { AxiosRequestConfig } from 'axios'

查看文件

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import type { AxiosRequestConfig } from 'axios'
import type { RequestError, RequestOptions } from './useRequest'
import type { RequestError } from './errors'
import type { RequestOptions } from './useRequest'
import { z } from 'zod'
import { useApi } from './useApi'

查看文件

@ -82,5 +82,7 @@ export async function getDeviceInfo(): Promise<DeviceInfo> {
model: String(C.Model ?? ''),
osVersion: String(C.Release ?? Platform.Version),
pushVendor: detectPushVendor(brand),
manufacturer: String(C.Manufacturer ?? undefined),
vendorVersion: undefined,
}
}

查看文件

@ -24,9 +24,10 @@ export {
openXWebView,
setXWebViewController,
setXWebViewNavigationRef,
setXWebViewScanQRCodeHandler,
processJSBridgeMessage,
} from './xwebview/XWebViewBridge'
export type { XWebViewNavigationRef } from './xwebview/XWebViewBridge'
export type { XWebViewNavigationRef, ScanQRCodeHandler } from './xwebview/XWebViewBridge'
export type {
XWebViewClickMenu,
XWebViewConfig,

查看文件

@ -171,6 +171,16 @@ true;`.trim()
break
}
case 'xuqm.scanQRCode': {
if (!_scanQRCodeHandler) {
respond(fail('scanQRCode not available — call setXWebViewScanQRCodeHandler() in host app'))
break
}
const result = await _scanQRCodeHandler()
respond(ok(result))
break
}
default:
// Unknown xuqm.* action — not handled by built-in bridge
return false

查看文件

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-native",
"baseUrl": "../../",
"paths": {
"@react-native-async-storage/async-storage": ["src/shims/async-storage.ts"]
},
"noEmit": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

24
packages/log/package.json 普通文件
查看文件

@ -0,0 +1,24 @@
{
"name": "@xuqm/rn-log",
"version": "0.1.0",
"description": "XuqmGroup RN SDK — log collection, error tracking, funnel analysis",
"license": "UNLICENSED",
"main": "src/index.ts",
"react-native": "src/index.ts",
"types": "src/index.ts",
"private": false,
"publishConfig": {
"registry": "https://nexus.xuqinmin.com/repository/npm-hosted/"
},
"scripts": {
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@xuqm/rn-common": ">=0.4.0",
"@react-native-async-storage/async-storage": ">=1.21.0",
"react-native": ">=0.76.0"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}

133
packages/log/src/XLog.ts 普通文件
查看文件

@ -0,0 +1,133 @@
import { Platform } from 'react-native'
import { getConfig, isInitialized, getUserId } from '@xuqm/rn-common'
import { LogQueue } from './queue/LogQueue'
import { ErrorCapture } from './capture/ErrorCapture'
import { computeFingerprint } from './fingerprint'
import { FunnelTracker } from './funnel/FunnelTracker'
import type { LogLevel, Environment, IssueEvent, LogEvent, XLogEvent } from './types'
// ─── Internal state ───────────────────────────────────────────────────────────
let _logLevel: LogLevel = 'warn'
let _environment: Environment = 'production'
let _queue: LogQueue | null = null
let _errorCaptureStarted = false
// Stable session id for the lifetime of the JS runtime
const _sessionId: string = _generateSessionId()
function _generateSessionId(): string {
return 'xxxx-xxxx'.replace(/x/g, () => ((Math.random() * 16) | 0).toString(16))
}
function _levelNum(level: LogLevel): number {
return { debug: 0, info: 1, warn: 2, error: 3 }[level]
}
function _getPlatform(): 'ios' | 'android' {
return Platform.OS === 'ios' ? 'ios' : 'android'
}
function _getAppVersion(): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const constants = Platform.constants as Record<string, unknown>
const v = constants['appVersion'] ?? constants['Version']
return typeof v === 'string' ? v : String(v ?? '0.0.0')
}
function _enqueue(event: XLogEvent): void {
if (!_queue) {
const cfg = getConfig()
if (!cfg.logApiUrl || !cfg.logEnabled) return
_queue = new LogQueue({ logApiUrl: cfg.logApiUrl, appKey: cfg.appKey })
}
void _queue.push(event)
}
// ─── XLog public API ──────────────────────────────────────────────────────────
export const XLog = {
/** Set the minimum log level. Events below this level are discarded. */
setLogLevel(level: LogLevel): void {
_logLevel = level
},
/** Set an environment tag attached to every log event. */
setEnvironment(env: Environment): void {
_environment = env
},
/**
* Record a custom analytics event (funnel analysis, behaviour tracking).
* Also automatically advances any matching funnel step.
*/
event(name: string, properties?: Record<string, unknown>): void {
if (!isInitialized()) return
const event: LogEvent = {
type: 'event',
name,
properties: properties ? { ...properties, environment: _environment } : { environment: _environment },
timestamp: Date.now(),
userId: getUserId() ?? undefined,
sessionId: _sessionId,
appKey: getConfig().appKey,
platform: _getPlatform(),
appVersion: _getAppVersion(),
}
_enqueue(event)
FunnelTracker.track(name, properties)
},
/** Upload a JS exception to the log service. */
captureError(error: unknown, metadata?: Record<string, unknown>): void {
if (!isInitialized()) return
const err = error instanceof Error ? error : new Error(String(error))
const issue: IssueEvent = {
type: 'js_error',
message: err.message,
stack: err.stack,
fingerprint: computeFingerprint('js_error', err.message, err.stack),
timestamp: Date.now(),
userId: getUserId() ?? undefined,
sessionId: _sessionId,
appKey: getConfig().appKey,
platform: _getPlatform(),
appVersion: _getAppVersion(),
metadata: metadata ? { ...metadata, environment: _environment } : { environment: _environment },
}
_enqueue(issue)
},
/** Log a warning (if log level allows). */
warn(message: string, metadata?: Record<string, unknown>): void {
if (_levelNum(_logLevel) > _levelNum('warn')) return
XLog.captureError(new Error(message), { ...metadata, level: 'warn' })
},
/** Log an informational event (if log level allows). */
info(message: string, metadata?: Record<string, unknown>): void {
if (_levelNum(_logLevel) > _levelNum('info')) return
XLog.event('__log_info', { message, ...metadata })
},
/**
* Enable automatic capture of JS global errors and unhandled Promise rejections.
* Call once at app startup after XuqmSDK.initialize().
*/
startCapture(): void {
if (_errorCaptureStarted) return
_errorCaptureStarted = true
ErrorCapture.start(XLog.captureError.bind(XLog))
},
/** Define a funnel for step-by-step conversion tracking. */
defineFunnel: FunnelTracker.define.bind(FunnelTracker),
/** Get client-side funnel progress (server aggregates across sessions). */
getFunnelProgress: FunnelTracker.getProgress.bind(FunnelTracker),
/** Flush any pending events immediately (e.g. before app goes to background). */
async flush(): Promise<void> {
if (_queue) await _queue.flush()
},
}

查看文件

@ -0,0 +1,40 @@
/**
* ErrorCapture hooks into the RN global error handler and unhandled promise
* rejections to automatically forward errors to XLog.captureError.
*/
// React Native exposes ErrorUtils on the global object
declare const ErrorUtils: {
getGlobalHandler(): ((error: Error, isFatal?: boolean) => void) | null
setGlobalHandler(handler: (error: Error, isFatal?: boolean) => void): void
}
// React Native / Hermes exposes onunhandledrejection on global
declare global {
// eslint-disable-next-line no-var
var onunhandledrejection:
| ((event: { reason: unknown }) => void)
| null
| undefined
}
export const ErrorCapture = {
start(onError: (error: unknown, meta?: Record<string, unknown>) => void): void {
// JS global error handler
const prevError =
typeof ErrorUtils !== 'undefined' ? ErrorUtils.getGlobalHandler() : null
if (typeof ErrorUtils !== 'undefined') {
ErrorUtils.setGlobalHandler((error, isFatal) => {
onError(error, { isFatal: isFatal ?? false })
prevError?.(error, isFatal)
})
}
// Unhandled Promise rejection
const prevUnhandled = global.onunhandledrejection
global.onunhandledrejection = (event: { reason: unknown }) => {
onError(event.reason, { type: 'unhandledRejection' })
prevUnhandled?.(event)
}
},
}

查看文件

@ -0,0 +1,149 @@
/**
* Pure-JS SHA-256 implementation.
* Hermes does not support Node.js `crypto` module, so we inline a minimal
* SHA-256 implementation compatible with the Hermes JS engine.
*/
// ─── SHA-256 ──────────────────────────────────────────────────────────────────
const K: number[] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]
function rotr32(x: number, n: number): number {
return ((x >>> n) | (x << (32 - n))) >>> 0
}
function sha256(message: string): string {
// Encode to UTF-8 bytes
const bytes: number[] = []
for (let i = 0; i < message.length; i++) {
let c = message.charCodeAt(i)
if (c < 0x80) {
bytes.push(c)
} else if (c < 0x800) {
bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f))
} else if (c < 0xd800 || c >= 0xe000) {
bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
} else {
// Surrogate pair
i++
c = 0x10000 + (((c & 0x3ff) << 10) | (message.charCodeAt(i) & 0x3ff))
bytes.push(
0xf0 | (c >> 18),
0x80 | ((c >> 12) & 0x3f),
0x80 | ((c >> 6) & 0x3f),
0x80 | (c & 0x3f),
)
}
}
const msgLen = bytes.length
bytes.push(0x80)
while ((bytes.length % 64) !== 56) bytes.push(0)
// Append length as 64-bit big-endian (we only support messages < 2^32 bytes)
const bitLen = msgLen * 8
bytes.push(0, 0, 0, 0)
bytes.push((bitLen >>> 24) & 0xff, (bitLen >>> 16) & 0xff, (bitLen >>> 8) & 0xff, bitLen & 0xff)
let h0 = 0x6a09e667
let h1 = 0xbb67ae85
let h2 = 0x3c6ef372
let h3 = 0xa54ff53a
let h4 = 0x510e527f
let h5 = 0x9b05688c
let h6 = 0x1f83d9ab
let h7 = 0x5be0cd19
const w = new Array<number>(64)
for (let i = 0; i < bytes.length; i += 64) {
for (let j = 0; j < 16; j++) {
w[j] =
((bytes[i + j * 4] << 24) |
(bytes[i + j * 4 + 1] << 16) |
(bytes[i + j * 4 + 2] << 8) |
bytes[i + j * 4 + 3]) >>>
0
}
for (let j = 16; j < 64; j++) {
const s0 = (rotr32(w[j - 15], 7) ^ rotr32(w[j - 15], 18) ^ (w[j - 15] >>> 3)) >>> 0
const s1 = (rotr32(w[j - 2], 17) ^ rotr32(w[j - 2], 19) ^ (w[j - 2] >>> 10)) >>> 0
w[j] = (w[j - 16] + s0 + w[j - 7] + s1) >>> 0
}
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7
for (let j = 0; j < 64; j++) {
const S1 = (rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25)) >>> 0
const ch = ((e & f) ^ (~e & g)) >>> 0
const temp1 = (h + S1 + ch + K[j] + w[j]) >>> 0
const S0 = (rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22)) >>> 0
const maj = ((a & b) ^ (a & c) ^ (b & c)) >>> 0
const temp2 = (S0 + maj) >>> 0
h = g; g = f; f = e
e = (d + temp1) >>> 0
d = c; c = b; b = a
a = (temp1 + temp2) >>> 0
}
h0 = (h0 + a) >>> 0
h1 = (h1 + b) >>> 0
h2 = (h2 + c) >>> 0
h3 = (h3 + d) >>> 0
h4 = (h4 + e) >>> 0
h5 = (h5 + f) >>> 0
h6 = (h6 + g) >>> 0
h7 = (h7 + h) >>> 0
}
return [h0, h1, h2, h3, h4, h5, h6, h7]
.map(v => v.toString(16).padStart(8, '0'))
.join('')
}
// ─── Public API ───────────────────────────────────────────────────────────────
function normalizeMessage(msg: string): string {
return msg.replace(/\b\d{4,}\b/g, 'N').replace(/[a-f0-9]{32,}/gi, 'H')
}
function extractTop3Frames(stack?: string): string {
if (!stack) return ''
return stack
.split('\n')
.filter(l => l.includes('at '))
.slice(0, 3)
.join('|')
}
/**
* Compute a SHA-256 fingerprint for deduplication.
* fingerprint = SHA-256(type + ":" + normalizedMessage + ":" + top3Frames)
*/
export function computeFingerprint(
type: string,
message: string,
stack?: string,
): string {
const top3 = extractTop3Frames(stack)
const raw = `${type}:${normalizeMessage(message)}:${top3}`
return sha256(raw)
}

查看文件

@ -0,0 +1,64 @@
/**
* FunnelTracker client-side funnel step tracking.
* Tracks which steps of a defined funnel have been completed in sequence.
* Final aggregation is done server-side using the events uploaded by LogQueue.
*/
interface FunnelDefinition {
id: string
steps: string[] // event names in order
}
interface FunnelProgress {
funnelId: string
completedSteps: string[]
completedAt?: number
}
const _funnels: Map<string, FunnelDefinition> = new Map()
const _progress: Map<string, FunnelProgress> = new Map()
export const FunnelTracker = {
/** Define a funnel with ordered steps. Resets progress if already defined. */
define(funnel: FunnelDefinition): void {
_funnels.set(funnel.id, funnel)
_progress.set(funnel.id, { funnelId: funnel.id, completedSteps: [] })
},
/**
* Called automatically by XLog.event for each event.
* Advances any funnel whose next expected step matches eventName.
*/
track(eventName: string, _properties?: Record<string, unknown>): void {
for (const [id, funnel] of _funnels) {
const progress = _progress.get(id)
if (!progress) continue
const nextStep = funnel.steps[progress.completedSteps.length]
if (nextStep === eventName) {
progress.completedSteps.push(eventName)
if (progress.completedSteps.length === funnel.steps.length) {
progress.completedAt = Date.now()
}
}
}
},
/** Get the current progress for a funnel (undefined if not defined). */
getProgress(funnelId: string): FunnelProgress | undefined {
return _progress.get(funnelId)
},
/** Reset a specific funnel's progress (e.g. on logout). */
reset(funnelId: string): void {
if (_funnels.has(funnelId)) {
_progress.set(funnelId, { funnelId, completedSteps: [] })
}
},
/** Reset all funnels' progress. */
resetAll(): void {
for (const [id] of _funnels) {
_progress.set(id, { funnelId: id, completedSteps: [] })
}
},
}

3
packages/log/src/index.ts 普通文件
查看文件

@ -0,0 +1,3 @@
export { XLog } from './XLog'
export type { LogLevel, Environment, LogEvent, IssueEvent, XLogEvent } from './types'
export type { UpdateDownloadProgress } from './types'

查看文件

@ -0,0 +1,51 @@
/**
* HttpInterceptor wraps the global fetch to automatically report
* HTTP error responses (4xx / 5xx) through XLog.captureError.
*
* Usage:
* HttpInterceptor.start(XLog.captureError.bind(XLog))
*/
type OnError = (error: unknown, meta?: Record<string, unknown>) => void
let _originalFetch: typeof fetch | null = null
export const HttpInterceptor = {
start(onError: OnError): void {
if (_originalFetch) return // already installed
_originalFetch = global.fetch
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(global as any).fetch = async (
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> => {
const original = _originalFetch!
try {
const res = await original(input, init)
if (!res.ok) {
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
onError(new Error(`HTTP ${res.status} ${res.statusText}`), {
type: 'api_error',
url,
status: res.status,
})
}
return res
} catch (e) {
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
onError(e, { type: 'api_error', url })
throw e
}
}
},
stop(): void {
if (_originalFetch) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(global as any).fetch = _originalFetch
_originalFetch = null
}
},
}

查看文件

@ -0,0 +1,26 @@
/**
* NativeLogReporter thin wrapper around the LogReporter TurboModule.
* Falls back gracefully if the native module is not linked.
*/
import { NativeModules } from 'react-native'
interface LogReporterModuleInterface {
startNativeCrashCapture(logApiUrl: string, appKey: string): void
stopNativeCrashCapture(): void
}
const _native = NativeModules.LogReporter as LogReporterModuleInterface | undefined
export const NativeLogReporter = {
isAvailable(): boolean {
return !!_native
},
startNativeCrashCapture(logApiUrl: string, appKey: string): void {
_native?.startNativeCrashCapture(logApiUrl, appKey)
},
stopNativeCrashCapture(): void {
_native?.stopNativeCrashCapture()
},
}

查看文件

@ -0,0 +1,75 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import type { XLogEvent } from '../types'
const QUEUE_KEY = '@xuqm_log:queue'
const BATCH_SIZE = 30
const FLUSH_INTERVAL_MS = 10_000 // 10 seconds
const MAX_QUEUE_SIZE = 500
export class LogQueue {
private flushTimer: ReturnType<typeof setInterval> | null = null
constructor(private cfg: { logApiUrl: string; appKey: string }) {
this.flushTimer = setInterval(() => {
void this.flush()
}, FLUSH_INTERVAL_MS)
}
async push(event: XLogEvent): Promise<void> {
const queue = await this._read()
if (queue.length >= MAX_QUEUE_SIZE) queue.shift() // drop oldest
queue.push(event)
await this._write(queue)
}
async flush(): Promise<void> {
const queue = await this._read()
if (queue.length === 0) return
const batch = queue.splice(0, BATCH_SIZE)
await this._write(queue) // remove from queue first to prevent duplicate sends
const issues = batch.filter(e => e.type !== 'event')
const events = batch.filter(e => e.type === 'event')
try {
if (issues.length > 0) await this._post('/log/v1/issues/batch', issues)
if (events.length > 0) await this._post('/log/v1/events/batch', events)
} catch {
// On failure, push batch back to front of queue (retry once on next flush)
const current = await this._read()
await this._write([...batch, ...current])
}
}
destroy(): void {
if (this.flushTimer !== null) {
clearInterval(this.flushTimer)
this.flushTimer = null
}
}
private async _post(path: string, data: XLogEvent[]): Promise<void> {
const body = JSON.stringify({ events: data })
const res = await fetch(`${this.cfg.logApiUrl}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-App-Key': this.cfg.appKey,
},
body,
})
if (!res.ok) {
throw new Error(`[XuqmLog] Upload failed: ${res.status}`)
}
}
private async _read(): Promise<XLogEvent[]> {
const raw = await AsyncStorage.getItem(QUEUE_KEY)
return raw ? (JSON.parse(raw) as XLogEvent[]) : []
}
private async _write(queue: XLogEvent[]): Promise<void> {
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue))
}
}

32
packages/log/src/types.ts 普通文件
查看文件

@ -0,0 +1,32 @@
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
export type Environment = 'development' | 'staging' | 'production'
export interface LogEvent {
type: 'event'
name: string
properties?: Record<string, unknown>
timestamp: number
userId?: string
sessionId: string
appKey: string
platform: 'ios' | 'android'
appVersion: string
fingerprint?: string
}
export interface IssueEvent {
type: 'js_error' | 'native_crash' | 'api_error' | 'warning'
message: string
stack?: string
fingerprint: string
count?: number
timestamp: number
userId?: string
sessionId: string
appKey: string
platform: 'ios' | 'android'
appVersion: string
metadata?: Record<string, unknown>
}
export type XLogEvent = LogEvent | IssueEvent

17
packages/log/tsconfig.json 普通文件
查看文件

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-native",
"baseUrl": "../../",
"paths": {
"@xuqm/rn-common": ["packages/common/src"],
"@react-native-async-storage/async-storage": ["src/shims/async-storage.ts"],
"@nozbe/watermelondb": ["src/shims/watermelondb.ts"],
"@nozbe/watermelondb/decorators": ["src/shims/watermelondb.ts"],
"@nozbe/watermelondb/adapters/sqlite": ["src/shims/watermelondb.ts"]
},
"noEmit": true
},
"include": ["src/**/*", "specs/**/*"],
"exclude": ["node_modules", "dist", "metro"]
}

查看文件

@ -1,6 +1,6 @@
{
"name": "@xuqm/rn-update",
"version": "0.3.0",
"version": "0.4.0",
"description": "XuqmGroup RN SDK — Update module (App update, RN plugin hot-update)",
"license": "UNLICENSED",
"main": "src/index.ts",

查看文件

@ -41,6 +41,13 @@ export interface PluginUpdateInfo {
changeLog?: string
}
export type UpdateDownloadProgress = {
bytesDownloaded: number
totalBytes: number
/** 0-100 */
percent: number
}
export type { CachedRnBundle }
interface CachedRnBundle {
moduleId: string
@ -54,6 +61,32 @@ interface CachedRnBundle {
const _pluginRegistry = new Set<string>()
// ─── Update check cache ────────────────────────────────────────────────────────
const UPDATE_APP_CACHE_KEY = 'xuqm_update_app_cache'
const UPDATE_PLUGIN_CACHE_KEY_PREFIX = 'xuqm_update_plugin_cache_'
const UPDATE_CACHE_TTL_MS = 30 * 60 * 1000 // 30 minutes
async function _readUpdateCache<T>(key: string): Promise<T | null> {
try {
const raw = await AsyncStorage.getItem(key)
if (!raw) return null
const cached = JSON.parse(raw) as { ts: number; data: T }
if (Date.now() - cached.ts < UPDATE_CACHE_TTL_MS) return cached.data
return null
} catch {
return null
}
}
async function _writeUpdateCache<T>(key: string, data: T): Promise<void> {
try {
await AsyncStorage.setItem(key, JSON.stringify({ ts: Date.now(), data }))
} catch {
// cache write failure is non-fatal
}
}
let _writeBundleCallback: ((moduleId: string, source: string) => Promise<void>) | null = null
let _reloadBundleCallback: ((moduleId: string) => Promise<void>) | null = null
@ -188,6 +221,12 @@ export const UpdateSDK = {
// ── App 整包更新 ──────────────────────────────────────────────────────────
async checkAppUpdate(bypassIgnore?: boolean): Promise<AppUpdateInfo> {
// Return cached result if within TTL (cache is skipped when bypassIgnore is set)
if (!bypassIgnore) {
const cached = await _readUpdateCache<AppUpdateInfo>(UPDATE_APP_CACHE_KEY)
if (cached) return cached
}
const config = getConfig()
const params: Record<string, string> = {
appKey: config.appKey,
@ -202,7 +241,9 @@ export const UpdateSDK = {
skipAuth: true,
params,
})
return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) }
const normalized = { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) }
await _writeUpdateCache(UPDATE_APP_CACHE_KEY, normalized)
return normalized
},
async openStore(appStoreUrl?: string, marketUrl?: string): Promise<void> {
@ -210,6 +251,96 @@ export const UpdateSDK = {
if (url) await Linking.openURL(url)
},
/**
* APK ArrayBuffer
* UpdateDownloadProgressoptions
*/
async downloadApk(
updateInfo: AppUpdateInfo,
options?: {
onProgress?: (progress: UpdateDownloadProgress) => void
},
): Promise<ArrayBuffer> {
const url = updateInfo.downloadUrl
if (!url) throw new Error('[UpdateSDK] downloadApk: no downloadUrl in updateInfo')
if (!options?.onProgress) {
return _downloadBinary(url)
}
// Streaming download with detailed progress
const response = await fetch(url)
if (!response.ok) throw new Error(`[UpdateSDK] downloadApk failed: ${response.status}`)
const contentLength = Number(response.headers.get('Content-Length') ?? '0')
const reader = response.body?.getReader()
if (!reader) return response.arrayBuffer()
const chunks: Uint8Array[] = []
let received = 0
for (;;) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
options.onProgress({
bytesDownloaded: received,
totalBytes: contentLength,
percent: contentLength > 0 ? Math.min(100, Math.round((received / contentLength) * 100)) : 0,
})
}
const totalLength = chunks.reduce((acc, c) => acc + c.length, 0)
const combined = new Uint8Array(totalLength)
let offset = 0
for (const chunk of chunks) {
combined.set(chunk, offset)
offset += chunk.length
}
return combined.buffer
},
/**
* bundle bundle JS
* UpdateDownloadProgressoptions
*/
async downloadPlugin(
moduleId: string,
updateInfo: PluginUpdateInfo,
options?: {
onProgress?: (progress: UpdateDownloadProgress) => void
},
): Promise<string> {
if (!updateInfo.downloadUrl) {
throw new Error(`[UpdateSDK] downloadPlugin(${moduleId}): no downloadUrl in updateInfo`)
}
if (!options?.onProgress) {
return _downloadText(updateInfo.downloadUrl)
}
// Streaming download with detailed progress
const response = await fetch(updateInfo.downloadUrl)
if (!response.ok) throw new Error(`[UpdateSDK] downloadPlugin failed: ${response.status}`)
const contentLength = Number(response.headers.get('Content-Length') ?? '0')
const reader = response.body?.getReader()
if (!reader) return response.text()
const chunks: Uint8Array[] = []
let received = 0
for (;;) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
options.onProgress({
bytesDownloaded: received,
totalBytes: contentLength,
percent: contentLength > 0 ? Math.min(100, Math.round((received / contentLength) * 100)) : 0,
})
}
const decoder = new TextDecoder()
return chunks.map(c => decoder.decode(c, { stream: true })).join('') + decoder.decode()
},
/**
* Android APK
*/
@ -247,6 +378,12 @@ export const UpdateSDK = {
'Call UpdateSDK.registerPlugins([{ moduleId }]) first.',
)
}
// Return cached result if within TTL
const cacheKey = `${UPDATE_PLUGIN_CACHE_KEY_PREFIX}${moduleId}`
const cached = await _readUpdateCache<PluginUpdateInfo>(cacheKey)
if (cached) return cached
const config = getConfig()
const currentVersion = await _getCachedVersion(moduleId)
const userId = getUserId()?.trim()
@ -262,11 +399,13 @@ export const UpdateSDK = {
skipAuth: true,
params,
})
return {
const normalized = {
...result,
currentVersion,
downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl,
}
await _writeUpdateCache(cacheKey, normalized)
return normalized
},
/**

查看文件

@ -1,2 +1,3 @@
export { UpdateSDK } from './UpdateSDK'
export type { PluginRegistration, PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from './UpdateSDK'
export type { PluginRegistration, PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle, UpdateDownloadProgress } from './UpdateSDK'
export { NativeBundle } from './NativeBundle'

查看文件

@ -1,5 +1,12 @@
export { getXWebViewConfig, openXWebView, setXWebViewController, XWebViewControl } from '@xuqm/rn-common'
export {
getXWebViewConfig,
openXWebView,
setXWebViewController,
setXWebViewScanQRCodeHandler,
XWebViewControl,
} from '@xuqm/rn-common'
export type {
ScanQRCodeHandler,
XWebViewClickMenu,
XWebViewConfig,
XWebViewControllerAPI,

192
yarn.lock
查看文件

@ -421,6 +421,13 @@ acorn@^8.15.0:
resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz"
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
agent-base@6:
version "6.0.2"
resolved "https://nexus.xuqinmin.com/repository/npm/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
agent-base@^7.1.2:
version "7.1.4"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
@ -453,6 +460,21 @@ asap@~2.0.6:
resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz"
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://nexus.xuqinmin.com/repository/npm/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.18.0, axios@^1.7.0:
version "1.18.0"
resolved "https://nexus.xuqinmin.com/repository/npm/axios/-/axios-1.18.0.tgz#8a7f8854af280fcaae063272df2ed9f3837d2398"
integrity sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==
dependencies:
follow-redirects "^1.16.0"
form-data "^4.0.5"
https-proxy-agent "^5.0.1"
proxy-from-env "^2.1.0"
babel-plugin-syntax-hermes-parser@0.33.3:
version "0.33.3"
resolved "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.33.3.tgz"
@ -522,6 +544,14 @@ buffer-from@^1.0.0:
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://nexus.xuqinmin.com/repository/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
camelcase@^5.0.0:
version "5.3.1"
resolved "https://nexus.xuqinmin.com/repository/npm/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@ -606,6 +636,13 @@ color-name@~1.1.4:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://nexus.xuqinmin.com/repository/npm/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
commander@^12.0.0:
version "12.1.0"
resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz"
@ -693,6 +730,11 @@ decode-uri-component@^0.2.2:
resolved "https://nexus.xuqinmin.com/repository/npm/decode-uri-component/-/decode-uri-component-0.2.2.tgz"
integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://nexus.xuqinmin.com/repository/npm/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
depd@2.0.0, depd@~2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
@ -738,6 +780,15 @@ domutils@^3.0.1:
domelementtype "^2.3.0"
domhandler "^5.0.3"
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://nexus.xuqinmin.com/repository/npm/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
@ -775,6 +826,33 @@ error-stack-parser@^2.0.6:
dependencies:
stackframe "^1.3.4"
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://nexus.xuqinmin.com/repository/npm/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.3.0:
version "1.3.0"
resolved "https://nexus.xuqinmin.com/repository/npm/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.2"
resolved "https://nexus.xuqinmin.com/repository/npm/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b"
integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==
dependencies:
es-errors "^1.3.0"
es-set-tostringtag@^2.1.0:
version "2.1.0"
resolved "https://nexus.xuqinmin.com/repository/npm/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
dependencies:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
has-tostringtag "^1.0.2"
hasown "^2.0.2"
escalade@^3.1.1, escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz"
@ -865,11 +943,32 @@ flow-enums-runtime@^0.0.6:
resolved "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz"
integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==
follow-redirects@^1.16.0:
version "1.16.0"
resolved "https://nexus.xuqinmin.com/repository/npm/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc"
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==
form-data@^4.0.5:
version "4.0.6"
resolved "https://nexus.xuqinmin.com/repository/npm/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827"
integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.4"
mime-types "^2.1.35"
fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://nexus.xuqinmin.com/repository/npm/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
@ -880,6 +979,30 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.2.6:
version "1.3.0"
resolved "https://nexus.xuqinmin.com/repository/npm/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://nexus.xuqinmin.com/repository/npm/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
glob@13.0.1:
version "13.0.1"
resolved "https://nexus.xuqinmin.com/repository/npm/glob/-/glob-13.0.1.tgz"
@ -889,6 +1012,11 @@ glob@13.0.1:
minipass "^7.1.2"
path-scurry "^2.0.0"
gopd@^1.2.0:
version "1.2.0"
resolved "https://nexus.xuqinmin.com/repository/npm/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
graceful-fs@^4.2.4, graceful-fs@^4.2.9:
version "4.2.11"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
@ -899,6 +1027,25 @@ has-flag@^4.0.0:
resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.1.0"
resolved "https://nexus.xuqinmin.com/repository/npm/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.2:
version "1.0.2"
resolved "https://nexus.xuqinmin.com/repository/npm/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies:
has-symbols "^1.0.3"
hasown@^2.0.2, hasown@^2.0.4:
version "2.0.4"
resolved "https://nexus.xuqinmin.com/repository/npm/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003"
integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==
dependencies:
function-bind "^1.1.2"
hermes-compiler@250829098.0.10:
version "250829098.0.10"
resolved "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.10.tgz"
@ -939,6 +1086,14 @@ http-errors@~2.0.1:
statuses "~2.0.2"
toidentifier "~1.0.1"
https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://nexus.xuqinmin.com/repository/npm/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
https-proxy-agent@^7.0.5:
version "7.0.6"
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz"
@ -1113,6 +1268,11 @@ marky@^1.2.2:
resolved "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz"
integrity sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://nexus.xuqinmin.com/repository/npm/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
mdn-data@2.0.14:
version "2.0.14"
resolved "https://nexus.xuqinmin.com/repository/npm/mdn-data/-/mdn-data-2.0.14.tgz"
@ -1336,11 +1496,23 @@ micromatch@^4.0.4:
braces "^3.0.3"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://nexus.xuqinmin.com/repository/npm/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-db@^1.54.0:
version "1.54.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz"
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
mime-types@^2.1.35:
version "2.1.35"
resolved "https://nexus.xuqinmin.com/repository/npm/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-types@^3.0.0, mime-types@^3.0.1:
version "3.0.2"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz"
@ -1533,6 +1705,11 @@ prop-types@^15.8.0:
object-assign "^4.1.1"
react-is "^16.13.1"
proxy-from-env@^2.1.0:
version "2.1.0"
resolved "https://nexus.xuqinmin.com/repository/npm/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba"
integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==
qrcode@^1.5.4:
version "1.5.4"
resolved "https://nexus.xuqinmin.com/repository/npm/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88"
@ -1668,6 +1845,11 @@ react-refresh@^0.14.0:
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz"
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
react@^19.0.0:
version "19.2.7"
resolved "https://nexus.xuqinmin.com/repository/npm/react/-/react-19.2.7.tgz#1f47a1bfc06f8ec885752c6f4af14369a9f8260b"
integrity sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==
regenerator-runtime@^0.13.2:
version "0.13.11"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz"
@ -2041,3 +2223,13 @@ yargs@^17.6.2:
string-width "^4.2.3"
y18n "^5.0.5"
yargs-parser "^21.1.1"
zod@3.23.8:
version "3.23.8"
resolved "https://nexus.xuqinmin.com/repository/npm/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
zod@^3.23.0:
version "3.25.76"
resolved "https://nexus.xuqinmin.com/repository/npm/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==