| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const DEFAULT_REQUIRED_MARKERS = [
- '空间名称',
- '关键尺寸',
- '门窗洞口',
- '梁柱/墙体限制',
- '水电/管道/烟道',
- '安装避让备注'
- ];
- const SAMPLE_ANALYSIS = {
- project: '实战二 P0 演示样本',
- image: 'sample-measurement-plan.jpg',
- sourceType: 'image',
- detectedMarkers: [
- {
- area: '厨房右侧墙面',
- markerType: '关键尺寸',
- value: '3200mm',
- evidence: '墙面横向尺寸已标注',
- confidence: 0.9
- },
- {
- area: '厨房水槽区域',
- markerType: '水电/管道/烟道',
- value: '下水管',
- evidence: '右下角有管道符号和文字',
- confidence: 0.82
- }
- ],
- missingMarkers: [
- {
- area: '厨房左侧门洞',
- missingType: '门洞宽度/高度',
- risk: '门洞尺寸缺失,后续柜体和动线复核容易出现偏差',
- evidence: '图中能看到门洞轮廓,但附近没有宽高数值',
- confidence: 0.86
- },
- {
- area: '窗户下沿',
- missingType: '窗台高度',
- risk: '窗台高度缺失,可能影响台面、吊柜或窗帘盒设计',
- evidence: '窗户位置已画出,但没有离地高度标注',
- confidence: 0.78
- }
- ],
- uncertainAreas: [
- {
- area: '厨房右上角',
- reason: '疑似烟道或立管,但图中文字不清晰',
- action: '请现场照片或原始量尺单复核',
- confidence: 0.58
- }
- ],
- overallJudgement: '存在关键缺标记,建议人工复核后再进入标准尺寸表。'
- };
- function parseArgs(argv) {
- const args = {};
- for (let i = 0; i < argv.length; i++) {
- const token = argv[i];
- if (!token.startsWith('--')) continue;
- const eq = token.indexOf('=');
- if (eq >= 0) {
- args[token.slice(2, eq)] = token.slice(eq + 1);
- continue;
- }
- const key = token.slice(2);
- const next = argv[i + 1];
- if (next && !next.startsWith('--')) {
- args[key] = next;
- i++;
- } else {
- args[key] = true;
- }
- }
- return args;
- }
- function splitList(value) {
- if (!value) return [];
- if (Array.isArray(value)) return value.map(String).map(item => item.trim()).filter(Boolean);
- return String(value).split(/[,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
- }
- function readJsonFile(filePath) {
- if (!filePath) return {};
- const absolute = path.resolve(filePath);
- if (!fs.existsSync(absolute)) {
- throw new Error(`Input JSON not found: ${absolute}`);
- }
- return JSON.parse(fs.readFileSync(absolute, 'utf8'));
- }
- function parseJsonValue(value, fallback) {
- if (!value) return fallback;
- if (Array.isArray(value) || typeof value === 'object') return value;
- return JSON.parse(String(value));
- }
- function asArray(value) {
- return Array.isArray(value) ? value : [];
- }
- function normalizeInput(input = {}) {
- const fromFile = readJsonFile(input.input || input['input-json']);
- const detectedMarkers = parseJsonValue(input.detectedMarkers || input['detected-markers'], fromFile.detectedMarkers || []);
- const missingMarkers = parseJsonValue(input.missingMarkers || input['missing-markers'], fromFile.missingMarkers || []);
- const uncertainAreas = parseJsonValue(input.uncertainAreas || input['uncertain-areas'], fromFile.uncertainAreas || []);
- return {
- ...fromFile,
- ...input,
- sample: Boolean(input.sample || fromFile.sample),
- project: input.project || fromFile.project,
- image: input.image || input.imageUrl || input.imagePath || fromFile.image || fromFile.imageUrl || fromFile.imagePath,
- imageUrl: input.imageUrl || input['image-url'] || fromFile.imageUrl,
- imagePath: input.imagePath || input['image-path'] || fromFile.imagePath,
- sourceType: input.sourceType || input['source-type'] || fromFile.sourceType,
- requiredMarkers: splitList(input.requiredMarkers || input['required-markers']).length
- ? splitList(input.requiredMarkers || input['required-markers'])
- : fromFile.requiredMarkers,
- detectedMarkers: asArray(detectedMarkers),
- missingMarkers: asArray(missingMarkers),
- uncertainAreas: asArray(uncertainAreas),
- overallJudgement: input.overallJudgement || input['overall-judgement'] || fromFile.overallJudgement
- };
- }
- function okResult(payload = {}) {
- return {
- status: 'ok',
- assistantMessage: payload.assistantMessage || '',
- summary: payload.summary || {},
- data: payload.data || {},
- files: payload.files || [],
- nextActions: payload.nextActions || [],
- warnings: payload.warnings || [],
- errors: payload.errors || []
- };
- }
- function errorResult(message, payload = {}) {
- return {
- status: 'error',
- assistantMessage: message,
- summary: payload.summary || {},
- data: payload.data || {},
- files: payload.files || [],
- nextActions: payload.nextActions || [],
- warnings: payload.warnings || [],
- errors: payload.errors || [{ message }]
- };
- }
- function normalizeConfidence(value) {
- if (typeof value !== 'number' || Number.isNaN(value)) return 0.7;
- return Math.max(0, Math.min(1, value));
- }
- function normalizeMarker(item = {}) {
- return {
- area: item.area || item.location || item.space || '未指明区域',
- markerType: item.markerType || item.type || item.label || '未分类标记',
- value: item.value || item.text || '',
- evidence: item.evidence || item.reason || '',
- confidence: normalizeConfidence(item.confidence)
- };
- }
- function normalizeMissing(item = {}) {
- return {
- area: item.area || item.location || item.space || '未指明区域',
- missingType: item.missingType || item.markerType || item.type || '未说明缺失项',
- risk: item.risk || item.impact || '需要人工复核,避免后续尺寸整理漏项。',
- evidence: item.evidence || item.reason || '',
- confidence: normalizeConfidence(item.confidence)
- };
- }
- function normalizeUncertain(item = {}) {
- return {
- area: item.area || item.location || item.space || '未指明区域',
- reason: item.reason || item.evidence || '图像信息不足,暂无法确认。',
- action: item.action || '建议回看原始量尺图或现场照片后复核。',
- confidence: normalizeConfidence(item.confidence)
- };
- }
- function countByType(items) {
- return items.reduce((acc, item) => {
- const key = item.markerType || item.missingType || '未分类';
- acc[key] = (acc[key] || 0) + 1;
- return acc;
- }, {});
- }
- function buildMeasurementMarkerReport(input = {}) {
- const source = input.sample ? SAMPLE_ANALYSIS : input;
- const detectedMarkers = asArray(source.detectedMarkers).map(normalizeMarker);
- const missingMarkers = asArray(source.missingMarkers).map(normalizeMissing);
- const uncertainAreas = asArray(source.uncertainAreas).map(normalizeUncertain);
- const requiredMarkers = asArray(source.requiredMarkers).length
- ? source.requiredMarkers.map(String)
- : DEFAULT_REQUIRED_MARKERS;
- const status = missingMarkers.length ? 'needs_review' : 'ok';
- const pass = status === 'ok' && uncertainAreas.length === 0;
- const summary = {
- project: source.project || '装修量尺图缺标记检查',
- image: source.image || source.imageUrl || source.imagePath || '',
- sourceType: source.sourceType || 'image',
- pass,
- status,
- detectedCount: detectedMarkers.length,
- missingCount: missingMarkers.length,
- uncertainCount: uncertainAreas.length,
- detectedByType: countByType(detectedMarkers),
- missingByType: countByType(missingMarkers)
- };
- const assistantMessage = renderAssistantMessage({
- summary,
- requiredMarkers,
- detectedMarkers,
- missingMarkers,
- uncertainAreas,
- overallJudgement: source.overallJudgement
- });
- return {
- summary,
- data: {
- requiredMarkers,
- detectedMarkers,
- missingMarkers,
- uncertainAreas,
- overallJudgement: source.overallJudgement || defaultJudgement(summary)
- },
- assistantMessage
- };
- }
- function defaultJudgement(summary) {
- if (summary.missingCount > 0) {
- return '当前量尺图存在疑似未标记位置,建议先人工复核缺失项,再输出标准化尺寸表。';
- }
- if (summary.uncertainCount > 0) {
- return '当前未发现明确缺标,但仍有图像不清晰区域,需要人工确认。';
- }
- return '当前未发现明显缺标记,可进入下一步标准化整理。';
- }
- function renderAssistantMessage({ summary, requiredMarkers, detectedMarkers, missingMarkers, uncertainAreas, overallJudgement }) {
- const lines = [];
- lines.push('## 量尺图缺标记检查(P0)');
- lines.push('');
- lines.push(`**结论**:${overallJudgement || defaultJudgement(summary)}`);
- lines.push('');
- lines.push(`- 已识别标记:${summary.detectedCount} 项`);
- lines.push(`- 疑似缺标记:${summary.missingCount} 项`);
- lines.push(`- 待确认区域:${summary.uncertainCount} 项`);
- lines.push('');
- lines.push('### 本轮检查口径');
- requiredMarkers.forEach(item => lines.push(`- ${item}`));
- lines.push('');
- lines.push('### 疑似未标记位置');
- if (missingMarkers.length) {
- missingMarkers.forEach((item, index) => {
- lines.push(`${index + 1}. ${item.area}:缺少「${item.missingType}」`);
- lines.push(` - 风险:${item.risk}`);
- if (item.evidence) lines.push(` - 图像依据:${item.evidence}`);
- lines.push(` - 置信度:${Math.round(item.confidence * 100)}%`);
- });
- } else {
- lines.push('- 暂未发现明确缺标记。');
- }
- lines.push('');
- if (uncertainAreas.length) {
- lines.push('### 待人工复核区域');
- uncertainAreas.forEach((item, index) => {
- lines.push(`${index + 1}. ${item.area}:${item.reason}`);
- lines.push(` - 建议动作:${item.action}`);
- lines.push(` - 置信度:${Math.round(item.confidence * 100)}%`);
- });
- lines.push('');
- }
- if (detectedMarkers.length) {
- lines.push('### 已识别标记摘录');
- detectedMarkers.slice(0, 8).forEach((item, index) => {
- const value = item.value ? `:${item.value}` : '';
- const evidence = item.evidence ? `(${item.evidence})` : '';
- lines.push(`${index + 1}. ${item.area} / ${item.markerType}${value}${evidence}`);
- });
- if (detectedMarkers.length > 8) {
- lines.push(`- 另有 ${detectedMarkers.length - 8} 项已识别标记写入结构化结果。`);
- }
- lines.push('');
- }
- lines.push('### 下一步');
- if (missingMarkers.length || uncertainAreas.length) {
- lines.push('- 先让量尺/设计同事确认上述缺标或模糊区域。');
- lines.push('- 复核后补齐尺寸、限制条件或备注,再生成标准化尺寸对照表。');
- } else {
- lines.push('- 可继续输出标准化尺寸对照表和异常预警清单。');
- }
- return lines.join('\n');
- }
- function writeMeasurementMarkerReport(outputDir, report) {
- const absolute = path.resolve(outputDir || path.join('outputs', 'measurement-marker-check', new Date().toISOString().slice(0, 10)));
- fs.mkdirSync(absolute, { recursive: true });
- const jsonPath = path.join(absolute, 'measurement-marker-check-result.json');
- const mdPath = path.join(absolute, 'measurement-marker-check-report.md');
- fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf8');
- fs.writeFileSync(mdPath, report.assistantMessage, 'utf8');
- return [jsonPath, mdPath];
- }
- async function runMeasurementMarkerCheck(input = {}) {
- const normalized = normalizeInput(input);
- const outputDir = path.resolve(normalized.output || path.join('outputs', 'measurement-marker-check', new Date().toISOString().slice(0, 10)));
- if (!normalized.sample && !normalized.image && !normalized.imageUrl && !normalized.imagePath) {
- return errorResult('请提供量尺图片路径/URL,或使用 --sample 跑 P0 演示样例。', {
- nextActions: [
- '上传或传入一张量尺图',
- 'Claude 先基于图片输出 detectedMarkers / missingMarkers / uncertainAreas',
- '再调用本工具生成 P0 缺标记报告'
- ]
- });
- }
- if (!normalized.sample && !normalized.detectedMarkers.length && !normalized.missingMarkers.length && !normalized.uncertainAreas.length) {
- return {
- status: 'needs_vision_observation',
- assistantMessage: [
- '已收到量尺图片入口,但还缺少看图后的结构化观察。',
- '',
- '请先让 Claude 直接观察上传的量尺图,提取:',
- '- detectedMarkers:已标记的空间、尺寸、门窗、管道、梁柱、备注等',
- '- missingMarkers:疑似未标记的位置、缺失类型、风险、图像依据、置信度',
- '- uncertainAreas:图像模糊或无法确定的位置',
- '',
- '随后把这些结构化结果传给本工具,即可生成 P0 缺标记报告。'
- ].join('\n'),
- summary: {
- image: normalized.image || normalized.imageUrl || normalized.imagePath,
- needsVisionObservation: true
- },
- data: {
- expectedInputShape: {
- detectedMarkers: [{ area: '厨房右侧墙面', markerType: '关键尺寸', value: '3200mm', evidence: '墙面横向尺寸已标注', confidence: 0.9 }],
- missingMarkers: [{ area: '窗户下沿', missingType: '窗台高度', risk: '影响台面或窗帘盒设计', evidence: '窗户有轮廓但未见离地高度', confidence: 0.78 }],
- uncertainAreas: [{ area: '右上角', reason: '文字模糊', action: '人工复核原图', confidence: 0.58 }]
- }
- },
- files: [],
- nextActions: ['先进行图片视觉识别', '再调用 measurement marker check 工具生成报告'],
- warnings: ['P0 阶段不在本地工具内直接调用视觉模型,由 Claude Code 视觉能力或上游识图服务提供观察结果。'],
- errors: []
- };
- }
- const report = buildMeasurementMarkerReport(normalized);
- const files = writeMeasurementMarkerReport(outputDir, report);
- return okResult({
- assistantMessage: report.assistantMessage,
- summary: report.summary,
- data: report.data,
- files,
- nextActions: report.summary.missingCount || report.summary.uncertainCount
- ? ['让量尺/设计同事补充缺标或模糊区域', '补齐后继续输出标准化尺寸对照表']
- : ['继续输出标准化尺寸对照表', '可选:与人工标准表做差异对比'],
- warnings: normalized.sample ? ['当前使用 P0 sample 演示数据。'] : []
- });
- }
- async function main() {
- const args = parseArgs(process.argv.slice(2));
- try {
- const result = await runMeasurementMarkerCheck(args);
- console.log(JSON.stringify(result, null, 2));
- if (args.resultPrefix || args['result-prefix']) {
- const prefix = args.resultPrefix || args['result-prefix'];
- console.log(`${prefix}=${JSON.stringify(result)}`);
- }
- process.exit(result.status === 'ok' || result.status === 'needs_vision_observation' ? 0 : 1);
- } catch (error) {
- const result = errorResult(error.message || String(error));
- console.log(JSON.stringify(result, null, 2));
- process.exit(1);
- }
- }
- if (require.main === module) {
- main();
- }
- module.exports = {
- runMeasurementMarkerCheck,
- buildMeasurementMarkerReport,
- SAMPLE_ANALYSIS,
- DEFAULT_REQUIRED_MARKERS
- };
|