Files
cadannotation/scripts/annotate-all-models.mjs
T

640 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 DEFAULT_SOURCE_ROOT = fs.existsSync(path.resolve(process.cwd(), '../json_data/original'))
? '../json_data/original'
: '../output_json 2';
const SOURCE_ROOT = path.resolve(process.cwd(), process.env.CADLABEL_SOURCE_ROOT ?? DEFAULT_SOURCE_ROOT);
const APPLY = process.argv.includes('--apply');
const FORCE = process.argv.includes('--force');
const onlyModel = valueAfter('--model');
const MARKER = '[AI批量分析:v1]';
const API_TIMEOUT_MS = Number(process.env.CADLABEL_API_TIMEOUT_MS ?? 120_000);
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(API_TIMEOUT_MS)
});
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, geometry) {
const configured = CLASS_DEFINITIONS.find((item) => item.ids.includes(modelName));
if (configured) return configured;
if (modelName.includes('方板+均布圆周孔')) {
return {
label: '圆周孔方形安装板',
scene: '方形安装基面上的圆周均布紧固、定位和载荷分散',
primaryFunction: '方板承载与圆周孔系连接'
};
}
if (modelName.includes('方板+四角孔')) {
return {
label: '多孔方形安装板',
scene: '方形安装基面上的四角紧固、中心定位和附加接口连接',
primaryFunction: '方板承载与多点安装'
};
}
if (modelName.includes('方台')) {
return {
label: geometry.interfaceCount ? '多孔方形安装台' : '方形承载台',
scene: '小型机械组件的平面承载、垫高、定位和紧固安装',
primaryFunction: '方台承载与安装定位'
};
}
if (modelName.includes('圆盘+均布孔')) {
return {
label: '圆周孔圆形安装盘',
scene: '圆形安装面上的周向均布紧固和同轴定位',
primaryFunction: '圆盘承载与圆周孔系连接'
};
}
if (modelName.includes('圆盘')) {
return {
label: '圆形承载盘',
scene: '圆形端面的承载、间隔和同轴安装',
primaryFunction: '圆盘承载与端面定位'
};
}
const sequenceNumber = /^\d+$/.test(modelName) ? Number(modelName) : null;
if (sequenceNumber !== null && sequenceNumber >= 40 && sequenceNumber <= 51) {
return {
label: '多孔圆形法兰盘',
scene: '中心接口与周向孔系组合的法兰连接和同轴定位',
primaryFunction: '圆形法兰连接与孔系定位'
};
}
if (sequenceNumber !== null && sequenceNumber >= 52 && sequenceNumber <= 58) {
return {
label: '双耳异形连接板',
scene: '双圆头或长圆形接口之间的跨距连接、铰接和定位',
primaryFunction: '双接口跨距连接与载荷传递'
};
}
if (sequenceNumber !== null && sequenceNumber >= 59 && sequenceNumber <= 66) {
return {
label: '多孔异形安装板',
scene: '多规格孔位的组合安装、定位和多点紧固',
primaryFunction: '异形板承载与多孔连接'
};
}
if (sequenceNumber !== null && sequenceNumber >= 67) {
return {
label: '复合孔系圆形法兰盘',
scene: '多圈、多规格孔系的法兰连接、精密定位和装配适配',
primaryFunction: '复合孔系法兰连接'
};
}
return geometry.outerCircular ? {
label: '多孔圆形安装盘',
scene: '圆形安装面上的承载、周向紧固和同轴定位',
primaryFunction: '圆盘承载与多孔连接'
} : {
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 geometryAnalysis(source) {
const box = source.validation_hints?.part_box_m;
const size = Array.isArray(box) && box.length === 6
? [box[3] - box[0], box[4] - box[1], box[5] - box[2]].map((value) => value * 1000)
: [];
const maxSpan = size.length ? Math.max(...size) : 0;
const faces = (source.features ?? []).flatMap((feature) => feature.owned_faces ?? []);
const cylinders = faces
.filter((face) => face.surface?.is_cylinder && Array.isArray(face.surface.cylinder_params))
.map((face) => {
const params = face.surface.cylinder_params;
const center = params.slice(0, 3).map((value) => value * 1000);
const axis = params.slice(3, 6);
const axisIndex = axis.reduce(
(best, value, index) => Math.abs(value) > Math.abs(axis[best] ?? 0) ? index : best,
0
);
const radialDistance = Math.hypot(...center.filter((_, index) => index !== axisIndex));
return { radius: Math.abs(params[6] * 1000), center, radialDistance };
});
const outerCircular = cylinders.some((item) =>
maxSpan > 0 && item.radius >= maxSpan * 0.4 && item.radialDistance <= maxSpan * 0.08
);
const interfaces = cylinders.filter((item) =>
maxSpan > 0 && item.radius <= maxSpan * 0.2
);
const uniqueInterfaces = new Map();
for (const item of interfaces) {
const key = `${formatNumber(item.radius)}:${item.center.map((value) => formatNumber(value)).join(',')}`;
uniqueInterfaces.set(key, item);
}
const diameterGroups = new Map();
for (const item of uniqueInterfaces.values()) {
const diameter = formatNumber(item.radius * 2);
diameterGroups.set(diameter, (diameterGroups.get(diameter) ?? 0) + 1);
}
const interfaceSummary = [...diameterGroups.entries()]
.sort((left, right) => Number(right[0]) - Number(left[0]))
.map(([diameter, count]) => `${count}ר${diameter} mm`)
.join('、');
return {
size,
outerCircular,
interfaceCount: uniqueInterfaces.size,
interfaceSummary
};
}
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, geometry) {
const evidence = dimensionEvidence(feature, step, modelName);
const faceCount = feature?.owned_faces?.length ?? 0;
const group = GROUP_DEFINITIONS[stepGroup(step)];
const compoundProfile = step.type === 'extrude_add' && geometry.interfaceCount > 0
? `该复合草图一次拉伸同时形成主体外轮廓与 ${geometry.interfaceCount} 个圆形内轮廓。`
: '';
return {
modelingDescription: `${operationIntent(step)}${compoundProfile}该步骤在本零件中归入“${group.name}”。`,
supplementDescription: [
`重建顺序第 ${step.order} 步,绑定源特征 ${step.sourceFeatureId}。`,
geometry.size.length ? `最终几何包络约为 ${geometry.size.map(formatNumber).join(' × ')} mm。` : null,
compoundProfile ? `圆形接口证据:${geometry.interfaceSummary}。` : null,
evidence.length ? `关键参数证据:${evidence.join('')}。` : '源特征未提供可直接解释的标量尺寸,语义依据操作类型与几何结果确定。',
faceCount ? `源特征记录 ${faceCount} 个归属面,可用于后续拓扑复核。` : '源特征未记录独占面,需结合前后步骤或最终拓扑复核。'
].filter(Boolean).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 geometryInterfaceStructurePayload(step, geometry, classInfo) {
return {
structureName: '复合草图孔系与圆弧接口',
structureType: '孔系/圆弧轮廓结构',
purpose: '在主体成形步骤中同步建立圆形内轮廓,用于紧固、定位、穿轴或装配避让。',
solution: `由建模步骤 ${step.order}.${step.name} 的复合草图一次拉伸形成;检测到 ${geometry.interfaceCount} 个圆形接口,直径分组为 ${geometry.interfaceSummary}。`,
reason: `复合草图将主体外轮廓与接口轮廓置于同一成形步骤,可保持相对位置一致;该判断同时结合“${classInfo.label}”的典型用途。`,
remark: `${MARKER} 圆形接口依据源特征归属面的圆柱几何识别;孔的用途、配合等级与加工公差需结合装配对象复核。`,
historyIds: [step.historyId]
};
}
function functionPayloads(classInfo, structures) {
const allIds = structures.map((item) => item.id);
const interfaceStructures = structures.filter((item) => ['cut', 'hole', 'thread', 'counterbore', 'pattern', 'geometry_interface'].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, geometry) {
const box = boundingBox(source);
const boxText = box ? `零件几何包络约为 ${box.join(' × ')} mm。` : '源数据未提供可解析的零件包络。';
const interfaceText = geometry.interfaceCount > 0
? `复合草图形成 ${geometry.interfaceCount} 个圆形接口,直径分组为 ${geometry.interfaceSummary}。`
: '未从源特征归属面中识别出独立的小直径圆形接口。';
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') || geometry.interfaceCount > 0;
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}${interfaceText}需保留建模历史中定义的主体、孔槽、连接接口和边缘处理,并保证各接口的相对位置与重建顺序一致。`,
designDescription: `${model.modelName} 被识别为${classInfo.label}${boxText}${interfaceText}有效建模序列为:${sequence}。主体加料特征建立承载包络,独立切除、孔向导或复合草图内轮廓形成装配接口,阵列保证重复结构一致,倒角/圆角改善边缘装配性。材料、载荷、配合公差和制造基准未由源 JSON 完整给出,相关结论均需工程复核。`
};
}
function hasText(value) {
return typeof value === 'string' && value.trim().length > 0;
}
function isCompleteAnnotation(exportData, historyCount) {
const annotatedCount = (exportData.featureIndex ?? []).filter((item) =>
hasText(item.annotation?.modelingIntent)
&& hasText(item.annotation?.description)
&& hasText(item.annotation?.notes)
).length;
return annotatedCount === historyCount
&& (exportData.structures?.length ?? 0) > 0
&& (exportData.functions?.length ?? 0) > 0
&& (exportData.unassignedHistoryIds?.length ?? 0) === 0;
}
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 && isCompleteAnnotation(exportData, histories.length)) {
return { model: model.modelName, status: 'skipped:already_complete' };
}
if (!FORCE && hasUnmanagedAnnotation(exportData)) {
return { model: model.modelName, status: 'skipped:existing_manual_annotation' };
}
const source = sourceFor(model.modelName);
const geometry = geometryAnalysis(source);
const classInfo = classFor(model.modelName, geometry);
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 geometryStep = geometry.interfaceCount > 0
? normalizedHistories.find((step) => step.type === 'extrude_add')
: null;
const modelUpdate = modelPayload(model, source, normalizedHistories, classInfo, geometry);
const dryRunStructures = [...groups.keys()].map((groupKey, index) => ({ id: index + 1, groupKey }));
if (geometryStep) dryRunStructures.push({ id: dryRunStructures.length + 1, groupKey: 'geometry_interface' });
if (!APPLY) {
return {
model: model.modelName,
status: 'dry-run',
histories: histories.length,
structures: dryRunStructures.length,
functions: functionPayloads(classInfo, dryRunStructures).length,
circularInterfaces: geometry.interfaceCount,
interfaceSummary: geometry.interfaceSummary || undefined
};
}
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), geometry)
);
}
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 });
}
if (geometryStep) {
const saved = await ensureStructure(
model.id,
geometryInterfaceStructurePayload(geometryStep, geometry, classInfo),
existingStructures
);
structures.push({ ...saved, groupKey: 'geometry_interface' });
}
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) =>
hasText(item.annotation?.modelingIntent)
&& hasText(item.annotation?.description)
&& hasText(item.annotation?.notes)
).length;
if (!isCompleteAnnotation(verified, histories.length)) {
throw new Error(`${model.modelName} 校验失败: annotated=${annotatedCount}/${histories.length}, structures=${verified.structures?.length ?? 0}, functions=${verified.functions?.length ?? 0}, 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 modelsByPage = [];
for (let pageNumber = 1; ; pageNumber += 1) {
const page = await api(`/cad-models?page=${pageNumber}&size=100`);
modelsByPage.push(...page.items);
if (modelsByPage.length >= page.total || page.items.length === 0) break;
}
let models = modelsByPage.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;
});