import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; const API_BASE = process.env.CADLABEL_API_BASE ?? 'http://127.0.0.1:8891/api'; const SOURCE_ROOT = path.resolve(process.cwd(), process.env.CADLABEL_SOURCE_ROOT ?? '../output_json 2'); const APPLY = process.argv.includes('--apply'); const FORCE = process.argv.includes('--force'); const onlyModel = valueAfter('--model'); const MARKER = '[AI批量分析:v1]'; const CLASS_DEFINITIONS = [ { ids: ['061735', '144358', '240860'], label: '圆形平板法兰盘', scene: '通用圆形安装连接面,适用于螺栓固定、端面贴合和中心定位', primaryFunction: '圆形法兰安装连接' }, { ids: ['020543', '080683', '202578', '029447'], label: '带凸台轴颈法兰', scene: '轴、套筒或设备端面的定位连接,利用凸台或轴颈建立同轴约束', primaryFunction: '法兰连接与同轴导向' }, { ids: ['080632', '086232', '191643'], label: '多孔工业圆法兰', scene: '多螺栓圆周连接、载荷分散和精密安装定位', primaryFunction: '多螺栓法兰连接' }, { ids: ['001563', '002935'], label: '筒体接头转接法兰', scene: '筒体、轴套或管状接口之间的同轴转接与支承', primaryFunction: '筒体接口转接与支承' }, { ids: ['001942', '218172'], label: '薄板环形/异形盲板', scene: '薄板封盖、垫片式隔离或异形基板固定', primaryFunction: '封盖隔离与异形安装' }, { ids: ['006947', '016033', '108524'], label: '矩形/方形安装法兰板', scene: '设备矩形安装面、轴孔定位和底板连接', primaryFunction: '设备安装与定位' }, { ids: ['026613', '177126', '214404', '221476'], label: '支架底座/耳板法兰', scene: '支架、底座、耳板或设备主体的承载安装连接', primaryFunction: '支架承载与底座固定' }, { ids: ['052729', '054798', '122225'], label: '轮毂衬套/厚环', scene: '轴孔导向、间隔定位、轮毂或厚壁环形连接', primaryFunction: '轴孔导向与间隔定位' }, { ids: ['015133'], label: '复杂多工位安装座', scene: '多凸台、多孔位机械组件的承载、定位与多方向装配', primaryFunction: '多工位承载与装配连接' }, { ids: ['083235'], label: '多层凸台圆形安装座', scene: '圆形基座、轴颈和多方向接口的同轴支承与紧固', primaryFunction: '圆形基座支承与多接口连接' } ]; const GROUP_DEFINITIONS = { base: { name: '主体承载基体', type: '主体结构', purpose: '建立零件的基本实体、总体包络和主要载荷传递路径。', reason: '首个加料特征决定零件的基础形态,也是后续孔、槽、凸台和边缘特征的宿主。' }, boss: { name: '凸台、轴颈与加强结构', type: '附加实体结构', purpose: '形成局部凸台、轴颈、台阶或加强区域,提供定位、支承或局部连接界面。', reason: '分层加料能够在控制总体重量的同时,把材料布置到定位和受力需要的位置。' }, cut: { name: '孔槽、内腔与避让结构', type: '切除结构', purpose: '形成通孔、内腔、槽口、轮廓减重或装配避让空间。', reason: '切除特征用于建立配合空间、功能通道和局部间隙,并避免与配合件干涉。' }, hole: { name: '安装与定位孔组', type: '孔系结构', purpose: '提供螺栓、销或轴类零件的安装、定位和穿过接口。', reason: '标准孔特征便于采用通用紧固件和加工刀具,并提高装配互换性。' }, thread: { name: '螺纹连接孔组', type: '螺纹接口结构', purpose: '提供内螺纹紧固接口,实现可拆卸连接和轴向夹紧。', reason: '孔向导定义的标准螺纹便于紧固件选型,并使连接规格可追溯。' }, counterbore: { name: '沉头紧固孔组', type: '沉头孔结构', purpose: '容纳螺钉杆部及头部,使紧固件可靠定位并控制头部突出量。', reason: '沉头或柱形沉头可提供稳定支承面,减少螺钉头与相邻零件的干涉。' }, pattern: { name: '阵列重复结构', type: '规则阵列结构', purpose: '按规则间距复制孔、槽或凸台,使重复结构保持一致。', reason: '参数化阵列保证重复特征的间距、数量和方向一致,便于均匀载荷或多工位装配。' }, edge: { name: '边缘过渡与装配处理', type: '边缘处理结构', purpose: '通过倒角或圆角改善装配导入、去除锐边并降低局部应力集中。', reason: '受控边缘过渡可提升装配性、加工安全性和边缘耐久性。' }, auxiliary: { name: '辅助几何结构', type: '辅助结构', purpose: '承载不能归入主体、孔槽或边缘处理的其他有效建模结果。', reason: '保留辅助步骤与结构的对应关系,避免建模历史在语义层丢失。' } }; function valueAfter(flag) { const index = process.argv.indexOf(flag); return index >= 0 ? process.argv[index + 1] : undefined; } async function api(relativePath, init) { const response = await fetch(`${API_BASE}${relativePath}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) }, signal: AbortSignal.timeout(30_000) }); const payload = await response.json(); if (!response.ok || payload.code !== 'OK') { throw new Error(`${init?.method ?? 'GET'} ${relativePath}: ${payload.message ?? response.status}`); } return payload.data; } function post(relativePath, body) { return api(relativePath, { method: 'POST', body: JSON.stringify(body) }); } function classFor(modelName) { return CLASS_DEFINITIONS.find((item) => item.ids.includes(modelName)) ?? { label: '机械安装连接件', scene: '机械组件的承载、定位和紧固连接', primaryFunction: '机械承载与装配连接' }; } function sourceFor(modelName) { const filename = path.join(SOURCE_ROOT, `${modelName}.solidworks_rebuild_extract.json`); if (!fs.existsSync(filename)) { throw new Error(`缺少源 JSON: ${filename}`); } return JSON.parse(fs.readFileSync(filename, 'utf8')); } function boundingBox(source) { const box = source.validation_hints?.part_box_m; if (!Array.isArray(box) || box.length !== 6) return null; const dimensions = [box[3] - box[0], box[4] - box[1], box[5] - box[2]].map((value) => value * 1000); return dimensions.map(formatNumber); } function formatNumber(value) { if (!Number.isFinite(value)) return String(value); return Number(value.toFixed(Math.abs(value) < 10 ? 3 : 2)).toString(); } function cleanDimensionName(name, modelName) { return String(name ?? '') .replace(new RegExp(`@${modelName}\\.Part$`, 'i'), '') .replace(/@[^@]+$/, ''); } function dimensionEvidence(feature, step, modelName) { const dimensions = feature?.dimensions ?? []; const evidence = []; for (const item of dimensions) { const value = Number(item.value); if (!Number.isFinite(value)) continue; const name = cleanDimensionName(item.name, modelName); const angleDimension = /角度|angle/i.test(item.name) || (step.type.includes('revolve') && item.name?.startsWith(`D1@${step.name}`)) || (step.type === 'chamfer' && item.name?.startsWith(`D2@${step.name}`)); if (angleDimension && Math.abs(value) > 20) { evidence.push(`${name}=${formatNumber(value / 1000 * 180 / Math.PI)}°`); continue; } if (step.type.includes('pattern') && Math.abs(value) >= 1000 && Math.abs(value) <= 20_000) { evidence.push(`${name}=${formatNumber(value / 1000)}(源阵列计数编码)`); continue; } if (Math.abs(value) > 1000) continue; evidence.push(`${name}=${formatNumber(value)} mm`); } return [...new Set(evidence)].slice(0, 10); } function stepGroup(step) { const type = String(step.type ?? step.operationType ?? '').toLowerCase(); const name = String(step.name ?? step.historyName ?? '').toLowerCase(); if (type === 'chamfer' || type === 'fillet') return 'edge'; if (type.includes('pattern')) return 'pattern'; if (type === 'hole') { if (/螺纹|thread|tapped/.test(name)) return 'thread'; if (/沉头|凹头|counter|螺钉/.test(name)) return 'counterbore'; return 'hole'; } if (type.includes('cut')) return 'cut'; if (type.includes('add')) return (step.order ?? step.historyIndex) === 1 ? 'base' : 'boss'; return 'auxiliary'; } function operationIntent(step) { const type = step.type.toLowerCase(); if (type === 'extrude_add') return `通过“${step.name}”执行加料拉伸,形成或扩展实体轮廓。`; if (type === 'revolve_add') return `通过“${step.name}”将截面绕中心轴旋转成形,建立回转主体、轴颈或环形台阶。`; if (type === 'extrude_cut') return `通过“${step.name}”执行拉伸切除,形成孔、槽、内腔或装配避让区域。`; if (type === 'revolve_cut') return `通过“${step.name}”执行旋转切除,形成同轴内孔、环槽、锥面或回转台阶。`; if (type === 'hole') return `通过孔向导特征“${step.name}”建立标准化孔接口,服务于紧固、定位或穿轴装配。`; if (type === 'linear_pattern') return `通过“${step.name}”按设定方向、间距和数量复制种子特征,形成规则重复结构。`; if (type === 'chamfer') return `通过“${step.name}”处理选定锐边,形成装配导入面并去除尖锐边缘。`; if (type === 'fillet') return `通过“${step.name}”对选定边进行圆角过渡,改善应力分布和边缘触感。`; return `执行建模步骤“${step.name}”,在前序几何基础上形成该步骤对应的有效几何。`; } function stepAnnotation(modelName, step, feature) { const evidence = dimensionEvidence(feature, step, modelName); const faceCount = feature?.owned_faces?.length ?? 0; const group = GROUP_DEFINITIONS[stepGroup(step)]; return { modelingDescription: `${operationIntent(step)}该步骤在本零件中归入“${group.name}”。`, supplementDescription: [ `重建顺序第 ${step.order} 步,绑定源特征 ${step.sourceFeatureId}。`, evidence.length ? `关键参数证据:${evidence.join(';')}。` : '源特征未提供可直接解释的标量尺寸,语义依据操作类型与几何结果确定。', faceCount ? `源特征记录 ${faceCount} 个归属面,可用于后续拓扑复核。` : '源特征未记录独占面,需结合前后步骤或最终拓扑复核。' ].join(''), remark: `${MARKER} 参数来自 SolidWorks 提取结果;角度、阵列计数及孔向导深度可能采用插件编码或构造深度,不应在未核对原模型前作为公差或有效加工深度。` }; } function groupSteps(histories) { const result = new Map(); for (const step of histories) { const key = stepGroup(step); if (!result.has(key)) result.set(key, []); result.get(key).push(step); } return result; } function structurePayload(groupKey, steps, classInfo) { const definition = GROUP_DEFINITIONS[groupKey]; const names = steps.map((step) => `${step.order}.${step.name}`).join('、'); return { structureName: definition.name, structureType: definition.type, purpose: definition.purpose, solution: `由建模步骤 ${names} 共同实现;具体尺寸和几何范围见各步骤标注。`, reason: `${definition.reason} 该判断同时结合“${classInfo.label}”的典型用途。`, remark: `${MARKER} 结构由实际建模步骤按几何作用归组;若后续获得装配图,应复核其主/辅助结构属性。`, historyIds: steps.map((step) => step.historyId) }; } function functionPayloads(classInfo, structures) { const allIds = structures.map((item) => item.id); const interfaceStructures = structures.filter((item) => ['cut', 'hole', 'thread', 'counterbore', 'pattern'].includes(item.groupKey)); const edgeStructures = structures.filter((item) => item.groupKey === 'edge'); const payloads = [{ functionName: classInfo.primaryFunction, functionType: '主功能', goalDescription: `使该${classInfo.label}在“${classInfo.scene}”场景下完成主体承载、几何定位和连接载荷传递。`, remark: `${MARKER} 主功能依据数据集分类、零件包络和完整建模历史综合推定,额定载荷与配合关系需结合装配数据确认。`, structureIds: allIds }]; if (interfaceStructures.length) { payloads.push({ functionName: '紧固、定位与装配适配', functionType: '装配功能', goalDescription: '利用孔、槽、螺纹、沉头或阵列接口约束配合件位置,容纳标准紧固件并提供必要装配间隙。', remark: `${MARKER} 具体紧固件规格、孔公差、配合等级和主辅接口关系需结合装配对象复核。`, structureIds: interfaceStructures.map((item) => item.id) }); } if (edgeStructures.length) { payloads.push({ functionName: '装配导入与边缘耐久', functionType: '辅助功能', goalDescription: '通过圆角和倒角减少锐边干涉,改善装配导入,并降低局部磕碰或应力集中风险。', remark: `${MARKER} 边缘处理功能依据特征类型确定;是否属于关键疲劳区域需结合载荷工况判断。`, structureIds: edgeStructures.map((item) => item.id) }); } return payloads; } function modelPayload(model, source, histories, classInfo) { const box = boundingBox(source); const boxText = box ? `零件几何包络约为 ${box.join(' × ')} mm。` : '源数据未提供可解析的零件包络。'; const sequence = histories.map((step) => `${step.order}.${step.name}(${step.type})`).join(';'); const hasRevolve = histories.some((step) => step.type.includes('revolve')); const hasHole = histories.some((step) => step.type === 'hole'); const manufacturing = [ hasRevolve ? '回转表面车削或车铣复合加工' : 'CNC 铣削或轮廓加工', hasHole ? '钻孔/扩孔/攻丝等孔系加工' : null, histories.some((step) => step.type === 'chamfer' || step.type === 'fillet') ? '边缘倒角/圆角处理' : null ].filter(Boolean).join(';'); return { applicationScenario: classInfo.scene, material: 'SolidWorks 源模型未指定材料;数据集建议铝合金或钢制测试件,需按载荷与环境确认', manufacturingMethod: `${manufacturing}(依据建模特征推定,待工艺确认)`, userRequirement: `设计一件${classInfo.label},用于${classInfo.scene}。${boxText}需保留建模历史中定义的主体、孔槽、连接接口和边缘处理,并保证各接口的相对位置与重建顺序一致。`, designDescription: `${model.modelName} 被识别为${classInfo.label}。${boxText}有效建模序列为:${sequence}。主体加料特征建立承载包络,切除与孔向导特征形成装配接口,阵列保证重复结构一致,倒角/圆角改善边缘装配性。材料、载荷、配合公差和制造基准未由源 JSON 完整给出,相关结论均需工程复核。` }; } function hasUnmanagedAnnotation(exportData) { const annotations = exportData.featureIndex ?? []; const existingNotes = annotations.map((item) => item.annotation?.notes).filter(Boolean); const structureNotes = (exportData.structures ?? []).map((item) => item.notes).filter(Boolean); const functionNotes = (exportData.functions ?? []).map((item) => item.notes).filter(Boolean); const notes = [...existingNotes, ...structureNotes, ...functionNotes]; return notes.some((note) => !String(note).startsWith(MARKER)); } async function ensureStructure(modelId, payload, existingByName) { const existing = existingByName.get(payload.structureName); if (!APPLY) return { id: `dry:${payload.structureName}`, groupKey: null }; const saved = existing ? await post(`/cad-models/${modelId}/structures/${existing.id}`, payload) : await post(`/cad-models/${modelId}/structures`, payload); existingByName.set(saved.structureName, saved); return saved; } async function ensureFunction(modelId, payload, existingByName) { const existing = existingByName.get(payload.functionName); if (!APPLY) return { id: `dry:${payload.functionName}` }; const saved = existing ? await post(`/cad-models/${modelId}/functions/${existing.id}`, payload) : await post(`/cad-models/${modelId}/functions`, payload); existingByName.set(saved.functionName, saved); return saved; } async function annotateModel(model) { const [histories, exportData] = await Promise.all([ api(`/cad-models/${model.id}/histories`), api(`/cad-models/${model.id}/export/annotation`) ]); if (!histories.length) return { model: model.modelName, status: 'skipped:no_histories' }; if (!FORCE && hasUnmanagedAnnotation(exportData)) { return { model: model.modelName, status: 'skipped:existing_manual_annotation' }; } const source = sourceFor(model.modelName); const classInfo = classFor(model.modelName); const featuresById = new Map((source.features ?? []).map((feature) => [feature.id, feature])); const normalizedHistories = histories.map((step) => ({ historyId: step.id, sourceFeatureId: step.sourceFeatureId, order: step.historyIndex, name: step.historyName, type: step.operationType })); const groups = groupSteps(normalizedHistories); const modelUpdate = modelPayload(model, source, normalizedHistories, classInfo); if (!APPLY) { return { model: model.modelName, status: 'dry-run', histories: histories.length, structures: groups.size, functions: functionPayloads(classInfo, [...groups.keys()].map((groupKey, index) => ({ id: index + 1, groupKey }))).length }; } await post(`/cad-models/${model.id}`, modelUpdate); for (const step of normalizedHistories) { await post( `/cad-models/${model.id}/histories/${step.historyId}/annotation`, stepAnnotation(model.modelName, step, featuresById.get(step.sourceFeatureId)) ); } const existingStructures = new Map((exportData.structures ?? []).map((item) => [item.name, { id: item.id }])); const structures = []; for (const [groupKey, steps] of groups) { const saved = await ensureStructure(model.id, structurePayload(groupKey, steps, classInfo), existingStructures); structures.push({ ...saved, groupKey }); } const existingFunctions = new Map((exportData.functions ?? []).map((item) => [item.name, { id: item.id }])); for (const payload of functionPayloads(classInfo, structures)) { await ensureFunction(model.id, payload, existingFunctions); } const verified = await api(`/cad-models/${model.id}/export/annotation`); const annotatedCount = (verified.featureIndex ?? []).filter((item) => item.annotation?.modelingIntent).length; if (annotatedCount !== histories.length || verified.unassignedHistoryIds?.length) { throw new Error(`${model.modelName} 校验失败: annotated=${annotatedCount}/${histories.length}, unassigned=${verified.unassignedHistoryIds}`); } return { model: model.modelName, status: 'applied', histories: histories.length, structures: verified.structures.length, functions: verified.functions.length, unassigned: verified.unassignedHistoryIds.length }; } async function main() { const page = await api('/cad-models?page=1&size=100'); let models = [...page.items].sort((left, right) => left.modelName.localeCompare(right.modelName)); if (onlyModel) models = models.filter((model) => model.modelName === onlyModel); if (!models.length) throw new Error(onlyModel ? `未找到模型 ${onlyModel}` : '数据库中没有模型'); console.log(`${APPLY ? 'APPLY' : 'DRY-RUN'} models=${models.length} sourceRoot=${SOURCE_ROOT}`); const results = []; for (const model of models) { const result = await annotateModel(model); results.push(result); console.log(JSON.stringify(result)); } const applied = results.filter((item) => item.status === 'applied').length; const dryRun = results.filter((item) => item.status === 'dry-run').length; const skipped = results.length - applied - dryRun; console.log(JSON.stringify({ total: results.length, applied, dryRun, skipped })); } main().catch((error) => { console.error(error.stack ?? error.message); process.exitCode = 1; });