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
这个提交包含在:
父节点
97d4d9498a
当前提交
16750b0421
@ -37,6 +37,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-native": "^0.73.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 { useApi } from './useApi'
|
||||||
export { usePageApi } from './usePageApi'
|
export { usePageApi } from './usePageApi'
|
||||||
export { RequestError } from './errors'
|
export { RequestError } from './errors'
|
||||||
export { setGlobalApiErrorHandler } from './globalErrorHandler'
|
export { setGlobalApiErrorHandler } from './globalErrorHandler'
|
||||||
export type { RequestOptions } from './useRequest'
|
export type { RequestOptions } from './useRequest'
|
||||||
export type { ApiMethod } from './useApi'
|
export type { ApiMethod } from './useApi'
|
||||||
|
export type { PageOptions } from './usePageApi'
|
||||||
export type { AxiosRequestConfig } from 'axios'
|
export type { AxiosRequestConfig } from 'axios'
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import type { AxiosRequestConfig } from 'axios'
|
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 { z } from 'zod'
|
||||||
|
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
|
|||||||
@ -82,5 +82,7 @@ export async function getDeviceInfo(): Promise<DeviceInfo> {
|
|||||||
model: String(C.Model ?? ''),
|
model: String(C.Model ?? ''),
|
||||||
osVersion: String(C.Release ?? Platform.Version),
|
osVersion: String(C.Release ?? Platform.Version),
|
||||||
pushVendor: detectPushVendor(brand),
|
pushVendor: detectPushVendor(brand),
|
||||||
|
manufacturer: String(C.Manufacturer ?? undefined),
|
||||||
|
vendorVersion: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,9 +24,10 @@ export {
|
|||||||
openXWebView,
|
openXWebView,
|
||||||
setXWebViewController,
|
setXWebViewController,
|
||||||
setXWebViewNavigationRef,
|
setXWebViewNavigationRef,
|
||||||
|
setXWebViewScanQRCodeHandler,
|
||||||
processJSBridgeMessage,
|
processJSBridgeMessage,
|
||||||
} from './xwebview/XWebViewBridge'
|
} from './xwebview/XWebViewBridge'
|
||||||
export type { XWebViewNavigationRef } from './xwebview/XWebViewBridge'
|
export type { XWebViewNavigationRef, ScanQRCodeHandler } from './xwebview/XWebViewBridge'
|
||||||
export type {
|
export type {
|
||||||
XWebViewClickMenu,
|
XWebViewClickMenu,
|
||||||
XWebViewConfig,
|
XWebViewConfig,
|
||||||
|
|||||||
@ -171,6 +171,16 @@ true;`.trim()
|
|||||||
break
|
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:
|
default:
|
||||||
// Unknown xuqm.* action — not handled by built-in bridge
|
// Unknown xuqm.* action — not handled by built-in bridge
|
||||||
return false
|
return false
|
||||||
|
|||||||
13
packages/common/tsconfig.json
普通文件
13
packages/common/tsconfig.json
普通文件
@ -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
普通文件
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
普通文件
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
149
packages/log/src/fingerprint.ts
普通文件
149
packages/log/src/fingerprint.ts
普通文件
@ -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
普通文件
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
普通文件
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
普通文件
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",
|
"name": "@xuqm/rn-update",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"description": "XuqmGroup RN SDK — Update module (App update, RN plugin hot-update)",
|
"description": "XuqmGroup RN SDK — Update module (App update, RN plugin hot-update)",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@ -41,6 +41,13 @@ export interface PluginUpdateInfo {
|
|||||||
changeLog?: string
|
changeLog?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateDownloadProgress = {
|
||||||
|
bytesDownloaded: number
|
||||||
|
totalBytes: number
|
||||||
|
/** 0-100 */
|
||||||
|
percent: number
|
||||||
|
}
|
||||||
|
|
||||||
export type { CachedRnBundle }
|
export type { CachedRnBundle }
|
||||||
interface CachedRnBundle {
|
interface CachedRnBundle {
|
||||||
moduleId: string
|
moduleId: string
|
||||||
@ -54,6 +61,32 @@ interface CachedRnBundle {
|
|||||||
|
|
||||||
const _pluginRegistry = new Set<string>()
|
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 _writeBundleCallback: ((moduleId: string, source: string) => Promise<void>) | null = null
|
||||||
let _reloadBundleCallback: ((moduleId: string) => Promise<void>) | null = null
|
let _reloadBundleCallback: ((moduleId: string) => Promise<void>) | null = null
|
||||||
|
|
||||||
@ -188,6 +221,12 @@ export const UpdateSDK = {
|
|||||||
// ── App 整包更新 ──────────────────────────────────────────────────────────
|
// ── App 整包更新 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async checkAppUpdate(bypassIgnore?: boolean): Promise<AppUpdateInfo> {
|
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 config = getConfig()
|
||||||
const params: Record<string, string> = {
|
const params: Record<string, string> = {
|
||||||
appKey: config.appKey,
|
appKey: config.appKey,
|
||||||
@ -202,7 +241,9 @@ export const UpdateSDK = {
|
|||||||
skipAuth: true,
|
skipAuth: true,
|
||||||
params,
|
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> {
|
async openStore(appStoreUrl?: string, marketUrl?: string): Promise<void> {
|
||||||
@ -210,6 +251,96 @@ export const UpdateSDK = {
|
|||||||
if (url) await Linking.openURL(url)
|
if (url) await Linking.openURL(url)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 APK 文件,返回 ArrayBuffer。
|
||||||
|
* 支持进度回调(UpdateDownloadProgress),向下兼容(options 可选)。
|
||||||
|
*/
|
||||||
|
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 源码)。
|
||||||
|
* 支持进度回调(UpdateDownloadProgress),向下兼容(options 可选)。
|
||||||
|
*/
|
||||||
|
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 直接下载并调起系统安装器。
|
* Android APK 直接下载并调起系统安装器。
|
||||||
*/
|
*/
|
||||||
@ -247,6 +378,12 @@ export const UpdateSDK = {
|
|||||||
'Call UpdateSDK.registerPlugins([{ moduleId }]) first.',
|
'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 config = getConfig()
|
||||||
const currentVersion = await _getCachedVersion(moduleId)
|
const currentVersion = await _getCachedVersion(moduleId)
|
||||||
const userId = getUserId()?.trim()
|
const userId = getUserId()?.trim()
|
||||||
@ -262,11 +399,13 @@ export const UpdateSDK = {
|
|||||||
skipAuth: true,
|
skipAuth: true,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
return {
|
const normalized = {
|
||||||
...result,
|
...result,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl,
|
downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl,
|
||||||
}
|
}
|
||||||
|
await _writeUpdateCache(cacheKey, normalized)
|
||||||
|
return normalized
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export { UpdateSDK } from './UpdateSDK'
|
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 {
|
export type {
|
||||||
|
ScanQRCodeHandler,
|
||||||
XWebViewClickMenu,
|
XWebViewClickMenu,
|
||||||
XWebViewConfig,
|
XWebViewConfig,
|
||||||
XWebViewControllerAPI,
|
XWebViewControllerAPI,
|
||||||
|
|||||||
192
yarn.lock
192
yarn.lock
@ -421,6 +421,13 @@ acorn@^8.15.0:
|
|||||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz"
|
resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz"
|
||||||
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
|
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:
|
agent-base@^7.1.2:
|
||||||
version "7.1.4"
|
version "7.1.4"
|
||||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
|
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"
|
resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz"
|
||||||
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
|
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:
|
babel-plugin-syntax-hermes-parser@0.33.3:
|
||||||
version "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"
|
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"
|
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
|
||||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
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:
|
camelcase@^5.0.0:
|
||||||
version "5.3.1"
|
version "5.3.1"
|
||||||
resolved "https://nexus.xuqinmin.com/repository/npm/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
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"
|
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
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:
|
commander@^12.0.0:
|
||||||
version "12.1.0"
|
version "12.1.0"
|
||||||
resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz"
|
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"
|
resolved "https://nexus.xuqinmin.com/repository/npm/decode-uri-component/-/decode-uri-component-0.2.2.tgz"
|
||||||
integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
|
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:
|
depd@2.0.0, depd@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
|
||||||
@ -738,6 +780,15 @@ domutils@^3.0.1:
|
|||||||
domelementtype "^2.3.0"
|
domelementtype "^2.3.0"
|
||||||
domhandler "^5.0.3"
|
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:
|
ee-first@1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||||
@ -775,6 +826,33 @@ error-stack-parser@^2.0.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
stackframe "^1.3.4"
|
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:
|
escalade@^3.1.1, escalade@^3.2.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz"
|
||||||
integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==
|
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:
|
fresh@~0.5.2:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz"
|
resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz"
|
||||||
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
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:
|
gensync@^1.0.0-beta.2:
|
||||||
version "1.0.0-beta.2"
|
version "1.0.0-beta.2"
|
||||||
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
|
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"
|
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
|
||||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
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:
|
glob@13.0.1:
|
||||||
version "13.0.1"
|
version "13.0.1"
|
||||||
resolved "https://nexus.xuqinmin.com/repository/npm/glob/-/glob-13.0.1.tgz"
|
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"
|
minipass "^7.1.2"
|
||||||
path-scurry "^2.0.0"
|
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:
|
graceful-fs@^4.2.4, graceful-fs@^4.2.9:
|
||||||
version "4.2.11"
|
version "4.2.11"
|
||||||
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
|
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"
|
resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
|
||||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
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:
|
hermes-compiler@250829098.0.10:
|
||||||
version "250829098.0.10"
|
version "250829098.0.10"
|
||||||
resolved "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.10.tgz"
|
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"
|
statuses "~2.0.2"
|
||||||
toidentifier "~1.0.1"
|
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:
|
https-proxy-agent@^7.0.5:
|
||||||
version "7.0.6"
|
version "7.0.6"
|
||||||
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz"
|
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"
|
resolved "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz"
|
||||||
integrity sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==
|
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:
|
mdn-data@2.0.14:
|
||||||
version "2.0.14"
|
version "2.0.14"
|
||||||
resolved "https://nexus.xuqinmin.com/repository/npm/mdn-data/-/mdn-data-2.0.14.tgz"
|
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"
|
braces "^3.0.3"
|
||||||
picomatch "^2.3.1"
|
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:
|
mime-db@^1.54.0:
|
||||||
version "1.54.0"
|
version "1.54.0"
|
||||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz"
|
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz"
|
||||||
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
|
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:
|
mime-types@^3.0.0, mime-types@^3.0.1:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz"
|
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"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.13.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:
|
qrcode@^1.5.4:
|
||||||
version "1.5.4"
|
version "1.5.4"
|
||||||
resolved "https://nexus.xuqinmin.com/repository/npm/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88"
|
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"
|
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz"
|
||||||
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
|
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:
|
regenerator-runtime@^0.13.2:
|
||||||
version "0.13.11"
|
version "0.13.11"
|
||||||
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz"
|
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"
|
string-width "^4.2.3"
|
||||||
y18n "^5.0.5"
|
y18n "^5.0.5"
|
||||||
yargs-parser "^21.1.1"
|
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==
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户