GuDingZiChan/backend/scripts/importParentChildMigrationToLive.js
2026-03-24 14:21:09 +08:00

855 lines
27 KiB
JavaScript

const fs = require('fs')
const path = require('path')
const ExcelJS = require(path.resolve(__dirname, '../node_modules/exceljs'))
const { resolveStorageLocationMatch } = require('./locationNormalization')
const OUTPUT_ROOT = path.resolve(__dirname, '../../temp_migration_output')
const DEFAULT_API_BASE = 'https://www.jsgdzc.cn/api'
const MIGRATION_MARKER = 'parent_child_excel_20260323'
const PARENT_BUILDABLE_STATUSES = new Set(['可用', '已借出', '维护中'])
const parseArgs = () => {
const args = process.argv.slice(2)
const options = {
mode: 'preflight',
planDir: '',
apiBase: DEFAULT_API_BASE,
username: '',
password: '',
limit: 0,
parentCode: '',
sourceRow: 0
}
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]
if (arg === '--mode') {
options.mode = args[index + 1] || options.mode
index += 1
} else if (arg === '--plan-dir') {
options.planDir = args[index + 1] || ''
index += 1
} else if (arg === '--api-base') {
options.apiBase = args[index + 1] || DEFAULT_API_BASE
index += 1
} else if (arg === '--username') {
options.username = args[index + 1] || ''
index += 1
} else if (arg === '--password') {
options.password = args[index + 1] || ''
index += 1
} else if (arg === '--limit') {
options.limit = Number(args[index + 1] || 0) || 0
index += 1
} else if (arg === '--parent-code') {
options.parentCode = args[index + 1] || ''
index += 1
} else if (arg === '--source-row') {
options.sourceRow = Number(args[index + 1] || 0) || 0
index += 1
}
}
return options
}
const normalizeText = (value) =>
String(value == null ? '' : value)
.replace(/\r/g, ' ')
.replace(/\n/g, ' ')
.replace(/\s+/g, ' ')
.trim()
const normalizeLookupKey = (value) => normalizeText(value).toUpperCase()
const normalizeMode = (value) => {
const mode = normalizeText(value).toLowerCase()
if (mode === 'apply') {
return 'apply'
}
return 'preflight'
}
const resolvePlanDir = (value) => {
if (normalizeText(value)) {
const resolved = path.isAbsolute(value) ? value : path.resolve(process.cwd(), value)
if (!fs.existsSync(resolved)) {
throw new Error(`计划目录不存在: ${resolved}`)
}
return resolved
}
const candidates = fs
.readdirSync(OUTPUT_ROOT, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name.startsWith('parent_child_dry_run_'))
.map((entry) => entry.name)
.sort()
if (!candidates.length) {
throw new Error(`未找到 parent_child_dry_run 输出目录: ${OUTPUT_ROOT}`)
}
return path.join(OUTPUT_ROOT, candidates[candidates.length - 1])
}
const ensureOutputDir = (prefix) => {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19)
const outputDir = path.join(OUTPUT_ROOT, `${prefix}_${timestamp}`)
fs.mkdirSync(outputDir, { recursive: true })
return outputDir
}
const readJsonFile = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'))
const writeJson = (filePath, value) => {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`)
}
const escapeCsv = (value) => {
const stringValue = String(value == null ? '' : value)
if (/[,"\n]/.test(stringValue)) {
return `"${stringValue.replace(/"/g, '""')}"`
}
return stringValue
}
const writeCsv = (filePath, headers, rows) => {
const lines = [headers.map(escapeCsv).join(',')]
for (const row of rows) {
lines.push(headers.map((header) => escapeCsv(row[header] ?? '')).join(','))
}
fs.writeFileSync(filePath, `\uFEFF${lines.join('\n')}`)
}
const buildSummaryRows = (summary) =>
Object.entries(summary).map(([key, value]) => ({
key,
value: Array.isArray(value) || (value && typeof value === 'object') ? JSON.stringify(value) : value
}))
const writeWorkbook = async (filePath, sheets) => {
const workbook = new ExcelJS.Workbook()
workbook.creator = 'Codex'
workbook.created = new Date()
for (const sheet of sheets) {
const rows = Array.isArray(sheet.rows) && sheet.rows.length ? sheet.rows : [{}]
const headers = [...new Set(rows.flatMap((row) => Object.keys(row)))]
const worksheet = workbook.addWorksheet(sheet.name)
worksheet.columns = headers.map((header) => ({
header,
key: header,
width: Math.min(Math.max(header.length + 4, 16), 48)
}))
rows.forEach((row) => worksheet.addRow(row))
worksheet.getRow(1).font = { bold: true }
worksheet.views = [{ state: 'frozen', ySplit: 1 }]
}
await workbook.xlsx.writeFile(filePath)
}
const loadPlan = (planDir, options) => {
const reportPath = path.join(planDir, 'report.json')
const summaryPath = path.join(planDir, 'summary.json')
if (!fs.existsSync(reportPath)) {
throw new Error(`未找到 report.json: ${reportPath}`)
}
const report = readJsonFile(reportPath)
const summary = fs.existsSync(summaryPath) ? readJsonFile(summaryPath) : report.summary || {}
let plannedChildren = Array.isArray(report.plannedChildren) ? report.plannedChildren : []
if (normalizeText(options.parentCode)) {
const parentCodeKey = normalizeLookupKey(options.parentCode)
plannedChildren = plannedChildren.filter((item) => normalizeLookupKey(item.parent_code) === parentCodeKey)
}
if (options.sourceRow > 0) {
plannedChildren = plannedChildren.filter((item) => Number(item.source_row) === Number(options.sourceRow))
}
if (options.limit > 0) {
plannedChildren = plannedChildren.slice(0, options.limit)
}
return {
planDir,
report,
summary,
plannedChildren
}
}
const sanitizeTokenValue = (value) => normalizeText(value).replace(/;/g, ',')
const buildActionKey = (item) =>
[
normalizeLookupKey(item.parent_code),
normalizeLookupKey(item.field_key),
Number(item.source_row) || 0,
Number(item.split_index) || 1
].join('||')
const buildChildTypeModelKey = (type, model) =>
`${normalizeLookupKey(type)}||${normalizeLookupKey(model)}`
const buildMigrationTokens = (item) => ({
migration_marker: MIGRATION_MARKER,
source_row: String(Number(item.source_row) || 0),
parent_code: sanitizeTokenValue(item.parent_code),
field_key: sanitizeTokenValue(item.field_key),
split_index: String(Number(item.split_index) || 1)
})
const buildChildDescription = (item) => {
const tokens = [
['migration_marker', MIGRATION_MARKER],
['source_row', Number(item.source_row) || 0],
['parent_code', item.parent_code],
['field_key', item.field_key],
['child_type', item.child_type],
['child_model', item.child_model],
['source_child_model', item.source_child_model || item.child_model],
['split_index', Number(item.split_index) || 1],
['split_quantity', Number(item.split_quantity) || 1],
['split_pattern', item.split_pattern || 'single_unit'],
['parent_name', item.parent_name],
['parent_type', item.parent_type],
['workbook_department', item.workbook_department],
['workbook_owner', item.workbook_owner],
['workbook_location', item.workbook_location]
]
return tokens.map(([key, value]) => `${key}=${sanitizeTokenValue(value)}`).join('; ')
}
const buildMountNote = (item) =>
Number(item.split_quantity) > 1
? `根据8.xlsx第${item.source_row}行补建${item.child_name || item.child_type}第${item.split_index}/${item.split_quantity}件,字段=${item.field_key},原始型号=${sanitizeTokenValue(item.source_child_model || item.child_model)},拆分后型号=${sanitizeTokenValue(item.child_model)}`
: `根据8.xlsx第${item.source_row}行补建${item.child_name || item.child_type},字段=${item.field_key},型号=${sanitizeTokenValue(item.child_model)}`
const descriptionMatchesPlan = (description, item) => {
const normalizedDescription = normalizeText(description)
if (!normalizedDescription) {
return false
}
const tokens = buildMigrationTokens(item)
return Object.entries(tokens).every(([key, value]) =>
normalizedDescription.includes(`${key}=${sanitizeTokenValue(value)}`)
)
}
const buildCookieHeader = (response) => {
if (typeof response.headers.getSetCookie === 'function') {
return response.headers.getSetCookie().map((item) => item.split(';')[0]).join('; ')
}
const setCookie = response.headers.get('set-cookie')
return setCookie ? setCookie.split(';')[0] : ''
}
class ApiSession {
constructor({ apiBase, username, password }) {
this.apiBase = normalizeText(apiBase).replace(/\/$/, '')
this.username = normalizeText(username)
this.password = String(password == null ? '' : password)
this.cookieHeader = ''
}
async login() {
if (!this.apiBase || !this.username || !this.password) {
throw new Error('缺少 API 登录参数')
}
const response = await fetch(`${this.apiBase}/auth/local/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: this.username,
password: this.password
})
})
const text = await response.text()
if (!response.ok) {
throw new Error(`登录失败: status=${response.status} body=${text}`)
}
this.cookieHeader = buildCookieHeader(response)
if (!this.cookieHeader) {
throw new Error('登录成功但未拿到 session cookie')
}
return JSON.parse(text)
}
async requestJson(endpoint, options = {}, retry = true) {
if (!this.cookieHeader) {
await this.login()
}
const headers = {
...(options.headers || {}),
Cookie: this.cookieHeader
}
const response = await fetch(`${this.apiBase}${endpoint}`, {
...options,
headers
})
const text = await response.text()
if (response.status === 401 && retry) {
await this.login()
return this.requestJson(endpoint, options, false)
}
if (!response.ok) {
throw new Error(`请求失败: ${endpoint} status=${response.status} body=${text}`)
}
return JSON.parse(text)
}
}
const unwrapListPayload = (payload) => {
if (Array.isArray(payload)) {
return payload
}
if (payload && Array.isArray(payload.list)) {
return payload.list
}
if (payload?.data && Array.isArray(payload.data.list)) {
return payload.data.list
}
throw new Error('接口返回结构不符合预期,未找到 list')
}
const fetchLiveData = async (session, outputDir, tag) => {
const [equipmentPayload, assetTypePayload, storageLocationPayload] = await Promise.all([
session.requestJson('/equipment/list?page=1&pageSize=5000'),
session.requestJson('/asset-types/list?page=1&pageSize=200'),
session.requestJson('/storage-locations/list?page=1&pageSize=200')
])
const equipmentSourcePath = path.join(outputDir, `live_equipment_${tag}.json`)
const assetTypeSourcePath = path.join(outputDir, `live_asset_types_${tag}.json`)
const storageLocationSourcePath = path.join(outputDir, `live_storage_locations_${tag}.json`)
writeJson(equipmentSourcePath, equipmentPayload)
writeJson(assetTypeSourcePath, assetTypePayload)
writeJson(storageLocationSourcePath, storageLocationPayload)
return {
equipmentList: unwrapListPayload(equipmentPayload),
assetTypes: unwrapListPayload(assetTypePayload),
storageLocations: unwrapListPayload(storageLocationPayload),
equipmentSourcePath,
assetTypeSourcePath,
storageLocationSourcePath
}
}
const buildLiveRefs = (liveData) => {
const topLevelByCode = new Map()
const topLevelItems = Array.isArray(liveData.equipmentList) ? liveData.equipmentList : []
for (const item of topLevelItems) {
const code = normalizeLookupKey(item.equipment_code)
if (code) {
topLevelByCode.set(code, item)
}
}
const assetTypeByName = new Map()
for (const item of liveData.assetTypes || []) {
assetTypeByName.set(normalizeLookupKey(item.name), item)
}
const locationByName = new Map()
for (const item of liveData.storageLocations || []) {
locationByName.set(normalizeLookupKey(item.name), item)
}
return {
topLevelItems,
topLevelByCode,
assetTypeByName,
locationByName
}
}
const summarizeByReason = (rows, reasonKey = 'reason') => {
const counts = {}
for (const row of rows) {
const reason = normalizeText(row[reasonKey]) || 'unknown'
counts[reason] = (counts[reason] || 0) + 1
}
return counts
}
const analyzePlan = (plan, liveData) => {
const refs = buildLiveRefs(liveData)
const actions = []
const blocked = []
const satisfied = []
const mountedChildQueueCache = new Map()
const getMountedChildQueue = (parent, key) => {
const cacheKey = `${parent.id}||${key}`
if (mountedChildQueueCache.has(cacheKey)) {
return mountedChildQueueCache.get(cacheKey)
}
const queue = (Array.isArray(parent.children) ? parent.children : []).filter(
(child) => buildChildTypeModelKey(child.equipment_type, child.equipment_model) === key
)
mountedChildQueueCache.set(cacheKey, queue)
return queue
}
for (const item of plan.plannedChildren) {
const parentCodeKey = normalizeLookupKey(item.parent_code)
const parent = refs.topLevelByCode.get(parentCodeKey)
const actionBase = {
action_key: buildActionKey(item),
source_row: item.source_row,
parent_code: item.parent_code,
parent_name: item.parent_name,
parent_type: item.parent_type,
parent_status: item.parent_status,
child_type: item.child_type,
child_name: item.child_name,
child_model: item.child_model,
source_child_model: item.source_child_model || item.child_model,
field_key: item.field_key,
split_index: Number(item.split_index) || 1,
split_quantity: Number(item.split_quantity) || 1,
split_pattern: item.split_pattern || 'single_unit',
notes: item.notes,
workbook_department: item.workbook_department,
workbook_owner: item.workbook_owner,
workbook_location: item.workbook_location,
planned_description: buildChildDescription(item),
planned_mount_note: buildMountNote(item)
}
const block = (reason, extra = {}) => {
blocked.push({
...actionBase,
reason,
...extra
})
}
if (!parent) {
block('live_parent_not_found')
continue
}
const liveParentType = refs.assetTypeByName.get(normalizeLookupKey(parent.equipment_type))
if (!liveParentType?.can_be_parent) {
block('live_parent_type_not_parent_capable', { live_parent_type: parent.equipment_type })
continue
}
if (parent.parent_id != null) {
block('live_parent_is_child', { live_parent_id: parent.parent_id })
continue
}
if (!PARENT_BUILDABLE_STATUSES.has(normalizeText(parent.status))) {
block('live_parent_status_not_buildable', { live_parent_status: parent.status })
continue
}
const childTypeConfig = refs.assetTypeByName.get(normalizeLookupKey(item.child_type))
if (!childTypeConfig?.can_be_child) {
block('child_asset_type_not_child_capable')
continue
}
const expectedChildKey = buildChildTypeModelKey(item.child_type, item.child_model)
const matchingMountedChildQueue = getMountedChildQueue(parent, expectedChildKey)
const matchingMountedChild = matchingMountedChildQueue.shift()
if (matchingMountedChild) {
satisfied.push({
...actionBase,
reason: 'already_mounted_matching_type_model',
live_parent_id: parent.id,
live_child_id: matchingMountedChild.id,
live_child_code: matchingMountedChild.equipment_code,
live_child_status: matchingMountedChild.status
})
continue
}
const orphanMatches = refs.topLevelItems.filter((equipment) => descriptionMatchesPlan(equipment.description, item))
if (orphanMatches.length > 1) {
block('duplicate_script_created_orphans', {
matched_orphan_codes: orphanMatches.map((equipment) => equipment.equipment_code).join(' | ')
})
continue
}
if (orphanMatches.length === 1) {
const orphan = orphanMatches[0]
if (buildChildTypeModelKey(orphan.equipment_type, orphan.equipment_model) !== expectedChildKey) {
block('script_created_orphan_model_mismatch', {
matched_orphan_code: orphan.equipment_code,
matched_orphan_type: orphan.equipment_type,
matched_orphan_model: orphan.equipment_model
})
continue
}
if (orphan.parent_id != null) {
block('script_created_orphan_already_mounted_elsewhere', {
matched_orphan_code: orphan.equipment_code,
matched_orphan_parent_id: orphan.parent_id
})
continue
}
if (normalizeText(orphan.status) !== '可用' || Number(orphan.remaining_quantity) <= 0) {
block('script_created_orphan_not_mountable', {
matched_orphan_code: orphan.equipment_code,
matched_orphan_status: orphan.status,
matched_orphan_remaining_quantity: orphan.remaining_quantity
})
continue
}
if (orphan.current_borrow) {
block('script_created_orphan_has_active_borrow', {
matched_orphan_code: orphan.equipment_code
})
continue
}
actions.push({
...actionBase,
action_type: 'mount_existing',
live_parent_id: parent.id,
existing_child_id: orphan.id,
existing_child_code: orphan.equipment_code,
parent_storage_location_id: parent.storage_location_id,
parent_department_id: parent.department_id
})
continue
}
const fallbackLocationResolution = !parent.storage_location_id
? resolveStorageLocationMatch(item.workbook_location, refs.locationByName)
: { match: null, normalizedValue: '', rule: '' }
const resolvedStorageLocationId = parent.storage_location_id || fallbackLocationResolution.match?.id || null
const resolvedStorageLocationName =
parent.storageLocation?.name ||
fallbackLocationResolution.match?.name ||
''
if (!resolvedStorageLocationId) {
block('live_parent_missing_storage_location', {
live_parent_id: parent.id,
workbook_location_resolution: fallbackLocationResolution.normalizedValue || '',
workbook_location_rule: fallbackLocationResolution.rule || ''
})
continue
}
actions.push({
...actionBase,
action_type: 'create_and_mount',
live_parent_id: parent.id,
parent_storage_location_id: resolvedStorageLocationId,
parent_storage_location_name: resolvedStorageLocationName,
parent_storage_location_source: parent.storage_location_id ? 'live_parent' : 'workbook_fallback',
parent_department_id: parent.department_id
})
}
const summary = {
planned_child_count: plan.plannedChildren.length,
actionable_count: actions.length,
actionable_create_and_mount_count: actions.filter((item) => item.action_type === 'create_and_mount').length,
actionable_mount_existing_count: actions.filter((item) => item.action_type === 'mount_existing').length,
satisfied_count: satisfied.length,
blocked_count: blocked.length,
blocked_reason_counts: summarizeByReason(blocked),
satisfied_reason_counts: summarizeByReason(satisfied)
}
return {
summary,
actions,
blocked,
satisfied
}
}
const buildCreateChildPayload = (action) => ({
entry_mode: 'child',
equipment_name: action.child_name,
equipment_model: action.child_model,
equipment_type: action.child_type,
code_generation_mode: 'auto',
unit_price: 0,
total_quantity: 1,
remaining_quantity: 1,
status: '可用',
storage_location_id: action.parent_storage_location_id,
department_id: action.parent_department_id || null,
description: action.planned_description
})
const applyActions = async (session, analysis) => {
const results = []
const errors = []
for (let index = 0; index < analysis.actions.length; index += 1) {
const action = analysis.actions[index]
const progress = `${index + 1}/${analysis.actions.length}`
console.log(`[${progress}] ${action.action_type} ${action.parent_code} ${action.child_type} ${action.child_model}`)
let createdChild = null
let stage = 'mount'
try {
let childId = action.existing_child_id || null
let childCode = action.existing_child_code || ''
if (action.action_type === 'create_and_mount') {
stage = 'create'
const createResult = await session.requestJson('/equipment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildCreateChildPayload(action))
})
createdChild = createResult?.data || null
childId = createdChild?.id || null
childCode = normalizeText(createdChild?.equipment_code)
if (!childId || !childCode) {
throw new Error('创建子资产返回的数据不完整')
}
}
stage = 'mount'
await session.requestJson(`/mount/${action.live_parent_id}/mount`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
child_id: childId,
notes: action.planned_mount_note
})
})
results.push({
...action,
apply_status: 'success',
child_id: childId,
child_code: childCode,
created_new_child: action.action_type === 'create_and_mount' ? 'yes' : 'no'
})
} catch (error) {
let cleanupStatus = 'not_needed'
let cleanupMessage = ''
if (action.action_type === 'create_and_mount' && createdChild?.id) {
cleanupStatus = 'pending'
try {
await session.requestJson(`/equipment/${createdChild.id}`, {
method: 'DELETE'
})
cleanupStatus = 'deleted'
} catch (cleanupError) {
cleanupStatus = 'delete_failed'
cleanupMessage = cleanupError?.message || String(cleanupError)
}
}
errors.push({
...action,
apply_status: 'failed',
failed_stage: stage,
error_message: error?.message || String(error),
created_child_id: createdChild?.id || '',
created_child_code: createdChild?.equipment_code || '',
cleanup_status: cleanupStatus,
cleanup_message: cleanupMessage
})
break
}
}
return {
results,
errors,
stopped_early: errors.length > 0
}
}
const writeAnalysisOutputs = async ({
outputDir,
plan,
liveData,
analysis,
execution = null,
postVerify = null,
mode
}) => {
const manifest = {
generated_at: new Date().toISOString(),
mode,
plan_dir: plan.planDir,
source_plan_summary: plan.summary,
selected_planned_child_count: plan.plannedChildren.length,
live_total_equipment: liveData.equipmentList.length,
live_total_asset_types: liveData.assetTypes.length,
live_total_storage_locations: (liveData.storageLocations || []).length,
analysis_summary: analysis.summary,
execution_summary: execution
? {
success_count: execution.results.length,
error_count: execution.errors.length,
stopped_early: execution.stopped_early
}
: null,
post_verify_summary: postVerify?.summary || null
}
writeJson(path.join(outputDir, 'manifest.json'), manifest)
writeJson(path.join(outputDir, 'analysis_summary.json'), analysis.summary)
writeJson(path.join(outputDir, 'blocked.json'), analysis.blocked)
writeJson(path.join(outputDir, 'satisfied.json'), analysis.satisfied)
writeJson(path.join(outputDir, 'actions.json'), analysis.actions)
writeCsv(
path.join(outputDir, 'actions.csv'),
Object.keys(analysis.actions[0] || { action_key: '', action_type: '', parent_code: '', child_type: '', child_model: '' }),
analysis.actions
)
writeCsv(
path.join(outputDir, 'blocked.csv'),
Object.keys(analysis.blocked[0] || { action_key: '', reason: '', parent_code: '', child_type: '', child_model: '' }),
analysis.blocked
)
writeCsv(
path.join(outputDir, 'satisfied.csv'),
Object.keys(analysis.satisfied[0] || { action_key: '', reason: '', parent_code: '', child_type: '', child_model: '' }),
analysis.satisfied
)
const workbookSheets = [
{ name: 'Summary', rows: buildSummaryRows(manifest) },
{ name: 'Actions', rows: analysis.actions },
{ name: 'Blocked', rows: analysis.blocked },
{ name: 'Satisfied', rows: analysis.satisfied },
{ name: 'OriginalSkippedRows', rows: plan.report.skippedRows || [] }
]
if (execution) {
writeJson(path.join(outputDir, 'apply_results.json'), execution.results)
writeJson(path.join(outputDir, 'apply_errors.json'), execution.errors)
writeCsv(
path.join(outputDir, 'apply_results.csv'),
Object.keys(execution.results[0] || { action_key: '', apply_status: '', child_code: '' }),
execution.results
)
writeCsv(
path.join(outputDir, 'apply_errors.csv'),
Object.keys(execution.errors[0] || { action_key: '', apply_status: '', error_message: '' }),
execution.errors
)
workbookSheets.push({ name: 'ApplyResults', rows: execution.results })
workbookSheets.push({ name: 'ApplyErrors', rows: execution.errors })
}
if (postVerify) {
writeJson(path.join(outputDir, 'post_verify.json'), postVerify)
workbookSheets.push({ name: 'PostVerifySummary', rows: buildSummaryRows(postVerify.summary || {}) })
workbookSheets.push({ name: 'PostVerifyBlocked', rows: postVerify.blocked || [] })
workbookSheets.push({ name: 'PostVerifyActions', rows: postVerify.actions || [] })
workbookSheets.push({ name: 'PostVerifySatisfied', rows: postVerify.satisfied || [] })
}
await writeWorkbook(path.join(outputDir, 'parent_child_live_migration.xlsx'), workbookSheets)
}
const run = async () => {
const options = parseArgs()
const mode = normalizeMode(options.mode)
const plan = loadPlan(resolvePlanDir(options.planDir), options)
const outputDir = ensureOutputDir(
mode === 'apply' ? 'parent_child_live_apply' : 'parent_child_live_preflight'
)
if (!plan.plannedChildren.length) {
throw new Error('筛选后没有可处理的 plannedChildren')
}
const session = new ApiSession({
apiBase: options.apiBase,
username: options.username,
password: options.password
})
const liveBefore = await fetchLiveData(session, outputDir, 'before')
const analysis = analyzePlan(plan, liveBefore)
let execution = null
let postVerify = null
if (mode === 'apply' && analysis.actions.length > 0) {
execution = await applyActions(session, analysis)
const liveAfter = await fetchLiveData(session, outputDir, 'after')
postVerify = analyzePlan(plan, liveAfter)
}
await writeAnalysisOutputs({
outputDir,
plan,
liveData: liveBefore,
analysis,
execution,
postVerify,
mode
})
console.log(
JSON.stringify(
{
outputDir,
mode,
selectedPlannedChildCount: plan.plannedChildren.length,
analysisSummary: analysis.summary,
executionSummary: execution
? {
successCount: execution.results.length,
errorCount: execution.errors.length,
stoppedEarly: execution.stopped_early
}
: null,
postVerifySummary: postVerify ? postVerify.summary : null
},
null,
2
)
)
}
run().catch((error) => {
console.error(error?.stack ? error.stack : String(error))
process.exit(1)
})