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:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -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)
}