SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
import { accessSync, constants, existsSync } from 'node:fs'
|
||||
import { mkdtemp, rm, readFile } from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { setTimeout as delay } from 'node:timers/promises'
|
||||
|
||||
export const chromeCandidates = [
|
||||
process.env.CHROME_BIN,
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
||||
'google-chrome',
|
||||
'chromium',
|
||||
'chrome'
|
||||
].filter(Boolean)
|
||||
|
||||
export function commandExists(command) {
|
||||
if (!command || command.includes('/')) return command && existsSync(command)
|
||||
return (process.env.PATH || '').split(path.delimiter).some((directory) => {
|
||||
if (!directory) return false
|
||||
try {
|
||||
accessSync(path.join(directory, command), constants.X_OK)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function findChrome() {
|
||||
return chromeCandidates.find(commandExists) || ''
|
||||
}
|
||||
|
||||
export function localChromeCdpStatus(chrome = findChrome()) {
|
||||
return {
|
||||
available: Boolean(chrome),
|
||||
executable: chrome || null,
|
||||
candidates: chromeCandidates.map((candidate) => ({
|
||||
command: candidate,
|
||||
available: commandExists(candidate)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export function codexInAppBrowserStatus() {
|
||||
return {
|
||||
available: false,
|
||||
checked: false,
|
||||
reason: 'Codex in-app Browser is not used by this Node verification script. Check agent.browsers.list() from an agent runtime before routing work to iab.'
|
||||
}
|
||||
}
|
||||
|
||||
export function browserExecutionContext(chrome = findChrome(), maxAttempts = 1) {
|
||||
return {
|
||||
mode: 'local Chrome/CDP',
|
||||
localChromeCdp: localChromeCdpStatus(chrome),
|
||||
codexInAppBrowser: codexInAppBrowserStatus(),
|
||||
boundedAttempts: Math.max(1, Number(maxAttempts) || 1)
|
||||
}
|
||||
}
|
||||
|
||||
export function classifyBrowserSetupError(error) {
|
||||
const message = String(error?.message || '')
|
||||
const rules = [
|
||||
['chrome-not-found', ['Chrome executable not found', 'Set CHROME_BIN']],
|
||||
['chrome-launch-failed', ['Chrome failed to start', 'Chrome exited before DevTools was ready']],
|
||||
['cdp-port-unavailable', ['Chrome did not expose DevToolsActivePort']],
|
||||
['cdp-websocket-unavailable', ['Timed out connecting to Chrome DevTools', 'Chrome DevTools WebSocket failed', 'No Chrome page target available', 'Chrome DevTools socket is not open']],
|
||||
['cdp-command-timeout', ['Timed out running Chrome DevTools command']],
|
||||
['runtime-auth-setup-failed', ['Chrome rejected runtime cookie', 'Unable to login to Modern API', 'Modern API login did not return a session cookie']],
|
||||
['base-url-unreachable', ['Modern UI base URL is not reachable', 'Modern UI base URL is not ready', 'Start OFBiz', 'MODERN_UI_BASE_URL']],
|
||||
['app-shell-unavailable', ['Modern UI app shell was not returned']]
|
||||
]
|
||||
for (const [category, needles] of rules) {
|
||||
if (needles.some((needle) => message.includes(needle))) return category
|
||||
}
|
||||
return 'unknown-browser-setup-error'
|
||||
}
|
||||
|
||||
export function isRetryableBrowserSetupError(error) {
|
||||
return new Set([
|
||||
'chrome-launch-failed',
|
||||
'cdp-port-unavailable',
|
||||
'cdp-websocket-unavailable',
|
||||
'cdp-command-timeout'
|
||||
]).has(classifyBrowserSetupError(error))
|
||||
}
|
||||
|
||||
export function createBrowserSetupTracker({ scope, maxAttempts, chrome = '', candidates = [] } = {}) {
|
||||
const attempts = []
|
||||
const boundedAttempts = Math.max(1, Number(maxAttempts) || 1)
|
||||
return {
|
||||
scope: scope || 'browser setup',
|
||||
maxAttempts: boundedAttempts,
|
||||
chrome: chrome || null,
|
||||
candidates,
|
||||
attempts,
|
||||
record({ attempt, chrome: attemptChrome = chrome, error, retryable } = {}) {
|
||||
const category = classifyBrowserSetupError(error)
|
||||
attempts.push({
|
||||
attempt,
|
||||
maxAttempts: boundedAttempts,
|
||||
chrome: attemptChrome || null,
|
||||
category,
|
||||
retryable: retryable ?? isRetryableBrowserSetupError(error),
|
||||
message: String(error?.message || error || 'unknown error')
|
||||
})
|
||||
},
|
||||
snapshot(extra = {}) {
|
||||
const last = attempts[attempts.length - 1] || null
|
||||
return {
|
||||
scope: scope || 'browser setup',
|
||||
mode: 'local Chrome/CDP',
|
||||
maxAttempts: boundedAttempts,
|
||||
attemptsUsed: attempts.length,
|
||||
chrome: chrome || null,
|
||||
candidates,
|
||||
attempts: attempts.slice(),
|
||||
lastCategory: last?.category || null,
|
||||
localChromeCdp: localChromeCdpStatus(chrome),
|
||||
codexInAppBrowser: codexInAppBrowserStatus(),
|
||||
boundedAttempts: boundedAttempts,
|
||||
...extra
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function summarizeBrowserSetupFailure({ scope, attempts = [], maxAttempts = 1, chrome = '', candidates = [], lastError = null } = {}) {
|
||||
const boundedAttempts = Math.max(1, Number(maxAttempts) || 1)
|
||||
const last = attempts[attempts.length - 1] || null
|
||||
const category = last?.category || classifyBrowserSetupError(lastError)
|
||||
const lines = [
|
||||
`Unable to initialize local Chrome/CDP${scope ? ` for ${scope}` : ''} after ${attempts.length || 0}/${boundedAttempts} bounded attempt(s).`,
|
||||
`Failure category: ${category}.`,
|
||||
'Codex in-app Browser (iab) status: not used by this Node script; do not retry iab unless agent.browsers.list() shows it is available.',
|
||||
`Local Chrome/CDP status: ${chrome ? `candidate ${chrome}` : 'no executable selected'}; ${candidates.length ? `${candidates.filter((item) => item.available).length}/${candidates.length} candidate(s) available` : 'candidate list unavailable'}.`,
|
||||
`Last error: ${last?.message || lastError?.message || 'unknown error'}.`
|
||||
]
|
||||
if (attempts.length) {
|
||||
lines.push(...attempts.map((attempt) => `attempt ${attempt.attempt}/${attempt.maxAttempts} [${attempt.category}] retryable=${attempt.retryable}: ${attempt.message}`))
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function waitForProcessExit(child, timeoutMs) {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true)
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
child.off('exit', onExit)
|
||||
resolve(false)
|
||||
}, timeoutMs)
|
||||
function onExit() {
|
||||
clearTimeout(timeout)
|
||||
resolve(true)
|
||||
}
|
||||
child.once('exit', onExit)
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForDevToolsPort(profile, chromeStartupTimeoutMs, getEarlyExit = () => null) {
|
||||
const activePortFile = path.join(profile, 'DevToolsActivePort')
|
||||
const deadline = Date.now() + chromeStartupTimeoutMs
|
||||
let lastError = null
|
||||
while (Date.now() < deadline) {
|
||||
const earlyExit = getEarlyExit()
|
||||
if (earlyExit) {
|
||||
if (earlyExit.error) {
|
||||
throw new Error(`Chrome failed to start: ${earlyExit.error.message}`)
|
||||
}
|
||||
throw new Error(`Chrome exited before DevTools was ready: code=${earlyExit.code ?? 'null'} signal=${earlyExit.signal ?? 'null'}`)
|
||||
}
|
||||
try {
|
||||
const text = await readFile(activePortFile, 'utf8')
|
||||
const [port] = text.trim().split('\n')
|
||||
if (port) return port
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
await delay(100)
|
||||
}
|
||||
throw new Error(lastError?.message || `Chrome did not expose DevToolsActivePort within ${chromeStartupTimeoutMs}ms`)
|
||||
}
|
||||
|
||||
export class CdpConnection {
|
||||
constructor(wsUrl, cdpCommandTimeoutMs = 10000) {
|
||||
this.wsUrl = wsUrl
|
||||
this.cdpCommandTimeoutMs = cdpCommandTimeoutMs
|
||||
this.nextId = 1
|
||||
this.pending = new Map()
|
||||
this.eventHandlers = new Map()
|
||||
}
|
||||
|
||||
async open() {
|
||||
this.socket = new WebSocket(this.wsUrl)
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out connecting to Chrome DevTools')), this.cdpCommandTimeoutMs)
|
||||
this.socket.addEventListener('open', () => {
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
}, { once: true })
|
||||
this.socket.addEventListener('error', () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error('Chrome DevTools WebSocket failed'))
|
||||
}, { once: true })
|
||||
})
|
||||
this.socket.addEventListener('message', (event) => {
|
||||
const message = JSON.parse(String(event.data))
|
||||
if (message.id && this.pending.has(message.id)) {
|
||||
const { resolve, reject, timeout } = this.pending.get(message.id)
|
||||
clearTimeout(timeout)
|
||||
this.pending.delete(message.id)
|
||||
if (message.error) {
|
||||
reject(new Error(message.error.message || JSON.stringify(message.error)))
|
||||
} else {
|
||||
resolve(message.result)
|
||||
}
|
||||
} else if (message.method) {
|
||||
for (const handler of this.eventHandlers.get(message.method) || []) {
|
||||
Promise.resolve(handler(message)).catch(() => {})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addEventListener(method, handler) {
|
||||
const handlers = this.eventHandlers.get(method) || []
|
||||
handlers.push(handler)
|
||||
this.eventHandlers.set(method, handlers)
|
||||
}
|
||||
|
||||
command(method, params = {}, timeoutMs = this.cdpCommandTimeoutMs) {
|
||||
const id = this.nextId++
|
||||
const payload = JSON.stringify({ id, method, params })
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
||||
reject(new Error(`Chrome DevTools socket is not open for ${method}`))
|
||||
return
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
this.pending.delete(id)
|
||||
reject(new Error(`Timed out running Chrome DevTools command: ${method}`))
|
||||
}, timeoutMs)
|
||||
this.pending.set(id, { resolve, reject, timeout })
|
||||
this.socket.send(payload)
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
for (const [id, pending] of this.pending.entries()) {
|
||||
clearTimeout(pending.timeout)
|
||||
pending.reject(new Error('Chrome DevTools connection closed'))
|
||||
this.pending.delete(id)
|
||||
}
|
||||
try {
|
||||
this.socket?.close()
|
||||
} catch {
|
||||
// Nothing to clean up.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function launchChrome(chrome, options = {}) {
|
||||
const chromeStartupTimeoutMs = Number(options.chromeStartupTimeoutMs || 15000)
|
||||
const profilePrefix = options.profilePrefix || 'ofbiz-modern-chrome.'
|
||||
const windowSize = options.windowSize || '1440,1000'
|
||||
const profile = await mkdtemp(path.join(os.tmpdir(), profilePrefix))
|
||||
const args = [
|
||||
'--headless=new',
|
||||
'--disable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-background-networking',
|
||||
'--disable-component-update',
|
||||
'--disable-sync',
|
||||
'--disable-extensions',
|
||||
'--remote-debugging-port=0',
|
||||
`--window-size=${windowSize}`,
|
||||
`--user-data-dir=${profile}`,
|
||||
'about:blank'
|
||||
]
|
||||
const child = spawn(chrome, args, { stdio: ['ignore', 'ignore', 'pipe'] })
|
||||
let stderr = ''
|
||||
let earlyExit = null
|
||||
let launchError = null
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk.toString()
|
||||
})
|
||||
child.once('error', (error) => {
|
||||
launchError = error
|
||||
})
|
||||
child.once('exit', (code, signal) => {
|
||||
earlyExit = { code, signal }
|
||||
})
|
||||
try {
|
||||
const port = await waitForDevToolsPort(profile, chromeStartupTimeoutMs, () => launchError ? { error: launchError } : earlyExit)
|
||||
const targets = await fetch(`http://127.0.0.1:${port}/json/list`).then((response) => response.json())
|
||||
const pageTarget = targets.find((target) => target.type === 'page' && target.webSocketDebuggerUrl)
|
||||
if (!pageTarget) throw new Error('No Chrome page target available')
|
||||
return {
|
||||
child,
|
||||
profile,
|
||||
wsUrl: pageTarget.webSocketDebuggerUrl,
|
||||
stderr: () => stderr
|
||||
}
|
||||
} catch (error) {
|
||||
try {
|
||||
child.kill('SIGTERM')
|
||||
} catch {
|
||||
// The process may never have spawned.
|
||||
}
|
||||
await waitForProcessExit(child, 1000)
|
||||
await rm(profile, { recursive: true, force: true })
|
||||
const stderrTail = stderr.trim().slice(-4000)
|
||||
throw new Error(stderrTail ? `${error.message}; Chrome stderr: ${stderrTail}` : error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeChrome(session) {
|
||||
if (session.child.exitCode === null && session.child.signalCode === null) {
|
||||
session.child.kill('SIGTERM')
|
||||
}
|
||||
const terminated = await waitForProcessExit(session.child, 1500)
|
||||
if (!terminated && session.child.exitCode === null && session.child.signalCode === null) {
|
||||
session.child.kill('SIGKILL')
|
||||
await waitForProcessExit(session.child, 1500)
|
||||
}
|
||||
await rm(session.profile, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })
|
||||
}
|
||||
|
||||
export async function evaluate(client, expression, timeoutMs = 5000) {
|
||||
const result = await client.command('Runtime.evaluate', {
|
||||
expression,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
timeout: timeoutMs
|
||||
})
|
||||
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Runtime.evaluate failed')
|
||||
return result.result?.value
|
||||
}
|
||||
|
||||
const consoleErrorCaptureSource = `
|
||||
(() => {
|
||||
const key = '__modernRuntimeConsoleErrors'
|
||||
const serialize = (value) => {
|
||||
try {
|
||||
if (typeof value === 'string') return value
|
||||
if (value && typeof value === 'object' && value.stack) return String(value.stack)
|
||||
if (value && typeof value === 'object' && value.message) return String(value.message)
|
||||
return JSON.stringify(value)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
const bucket = Array.isArray(window[key]) ? window[key] : []
|
||||
try {
|
||||
Object.defineProperty(window, key, { value: bucket, configurable: true, writable: true })
|
||||
} catch {
|
||||
window[key] = bucket
|
||||
}
|
||||
if (window.__modernRuntimeConsoleCaptureInstalled) return
|
||||
try {
|
||||
Object.defineProperty(window, '__modernRuntimeConsoleCaptureInstalled', { value: true, configurable: true })
|
||||
} catch {
|
||||
window.__modernRuntimeConsoleCaptureInstalled = true
|
||||
}
|
||||
const originalError = console.error ? console.error.bind(console) : () => {}
|
||||
console.error = (...args) => {
|
||||
bucket.push(args.map(serialize).filter(Boolean).join(' '))
|
||||
return originalError(...args)
|
||||
}
|
||||
window.addEventListener('error', (event) => {
|
||||
bucket.push(event.message || (event.error && serialize(event.error)) || 'window error')
|
||||
})
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
bucket.push(event.reason ? serialize(event.reason) : 'unhandled rejection')
|
||||
})
|
||||
})()
|
||||
`
|
||||
|
||||
function remoteObjectText(remoteObject) {
|
||||
if (!remoteObject) return ''
|
||||
if (typeof remoteObject.description === 'string') return remoteObject.description
|
||||
if (remoteObject.value !== undefined) return String(remoteObject.value)
|
||||
if (remoteObject.unserializableValue !== undefined) return String(remoteObject.unserializableValue)
|
||||
return remoteObject.type || ''
|
||||
}
|
||||
|
||||
function uniqueConsoleErrors(errors) {
|
||||
const seen = new Set()
|
||||
const unique = []
|
||||
for (const error of errors) {
|
||||
const text = String(error || '').trim()
|
||||
if (!text || seen.has(text)) continue
|
||||
seen.add(text)
|
||||
unique.push(text)
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
export async function installConsoleErrorCapture(client) {
|
||||
const eventErrors = []
|
||||
|
||||
client.addEventListener('Runtime.consoleAPICalled', (event) => {
|
||||
if (event.params?.type !== 'error') return
|
||||
const text = (event.params?.args || []).map(remoteObjectText).filter(Boolean).join(' ')
|
||||
eventErrors.push(text || 'console.error')
|
||||
})
|
||||
|
||||
client.addEventListener('Runtime.exceptionThrown', (event) => {
|
||||
const details = event.params?.exceptionDetails || {}
|
||||
const text = remoteObjectText(details.exception) || details.text || 'runtime exception'
|
||||
eventErrors.push(text)
|
||||
})
|
||||
|
||||
await client.command('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source: consoleErrorCaptureSource
|
||||
})
|
||||
|
||||
return {
|
||||
eventErrors: () => uniqueConsoleErrors(eventErrors),
|
||||
async reset() {
|
||||
eventErrors.length = 0
|
||||
await evaluate(client, 'window.__modernRuntimeConsoleErrors = []').catch(() => undefined)
|
||||
},
|
||||
async snapshot() {
|
||||
const pageErrors = await evaluate(client, 'window.__modernRuntimeConsoleErrors || []').catch(() => [])
|
||||
return uniqueConsoleErrors([
|
||||
...eventErrors,
|
||||
...(Array.isArray(pageErrors) ? pageErrors : [])
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForExpression(client, expression, timeoutMs = 15000) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() < deadline) {
|
||||
const value = await evaluate(client, `Boolean(${expression})`, 2000).catch(() => false)
|
||||
if (value) return
|
||||
await delay(150)
|
||||
}
|
||||
throw new Error(`Timed out waiting for expression: ${expression}`)
|
||||
}
|
||||
|
||||
const fetchMockState = new WeakMap()
|
||||
|
||||
export function installSessionMock(client, sessionPayload) {
|
||||
return installApiMock(client, {
|
||||
'*://*/api/v1/session*': sessionPayload
|
||||
})
|
||||
}
|
||||
|
||||
export async function installApiMock(client, handlers) {
|
||||
const entries = Object.entries(handlers || {})
|
||||
if (!entries.length) return
|
||||
const state = fetchMockState.get(client) || {
|
||||
handlers: new Map(),
|
||||
listening: false
|
||||
}
|
||||
for (const [urlPattern, handler] of entries) {
|
||||
state.handlers.set(urlPattern, handler)
|
||||
}
|
||||
fetchMockState.set(client, state)
|
||||
|
||||
await client.command('Fetch.enable', {
|
||||
patterns: Array.from(state.handlers.keys()).map((urlPattern) => ({ urlPattern, requestStage: 'Request' }))
|
||||
})
|
||||
|
||||
if (state.listening) return
|
||||
state.listening = true
|
||||
client.addEventListener('Fetch.requestPaused', async (event) => {
|
||||
const requestId = event.params?.requestId
|
||||
const url = event.params?.request?.url || ''
|
||||
if (!requestId) return
|
||||
const activeState = fetchMockState.get(client) || state
|
||||
const match = Array.from(activeState.handlers.entries()).find(([urlPattern]) => urlMatchesPattern(url, urlPattern))
|
||||
if (!match) {
|
||||
await client.command('Fetch.continueRequest', { requestId }).catch(() => {})
|
||||
return
|
||||
}
|
||||
const payload = typeof match[1] === 'function' ? match[1](url, event.params?.request || {}) : match[1]
|
||||
await client.command('Fetch.fulfillRequest', {
|
||||
requestId,
|
||||
responseCode: 200,
|
||||
responseHeaders: [
|
||||
{ name: 'Content-Type', value: 'application/json; charset=utf-8' },
|
||||
{ name: 'Cache-Control', value: 'no-store' }
|
||||
],
|
||||
body: Buffer.from(JSON.stringify(payload), 'utf8').toString('base64')
|
||||
}).catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
function urlMatchesPattern(url, pattern) {
|
||||
const escaped = String(pattern)
|
||||
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*')
|
||||
return new RegExp(`^${escaped}$`).test(url)
|
||||
}
|
||||
|
||||
export async function runtimeDiagnostics(client, id) {
|
||||
const value = await evaluate(client, `(() => {
|
||||
const app = document.querySelector('#app')
|
||||
return {
|
||||
id: ${JSON.stringify(id)},
|
||||
href: location.href,
|
||||
readyState: document.readyState,
|
||||
hasApp: Boolean(app),
|
||||
appChildCount: app ? app.childElementCount : -1,
|
||||
appHtml: app ? app.innerHTML.slice(0, 800) : '',
|
||||
bodyText: document.body ? document.body.innerText.slice(0, 1200) : '',
|
||||
consoleErrors: window.__modernRuntimeConsoleErrors || []
|
||||
}
|
||||
})()`, 2000).catch((error) => ({
|
||||
id,
|
||||
evaluateError: error.message
|
||||
}))
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
Reference in New Issue
Block a user