@gangvy/claude-code-measurement-marker-check 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -0
- package/README.md +46 -0
- package/bin/claude-measure-marker.js +148 -0
- package/package.json +23 -0
- package/scripts/smoke.js +49 -0
- package/skill-package-manifest.json +13 -0
- package/skills/measurement-marker-check/SKILL.md +125 -0
- package/skills/measurement-marker-check/references/fmode-vision-reference.md +43 -0
- package/tools/measurement-marker-check-run.js +414 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "measurement-marker-check",
|
|
3
|
+
"description": "P0 renovation measurement drawing marker check skill for Claude Code. Upload a measurement image, identify missing annotations, and produce a review checklist.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "nkkj-BrainHack"
|
|
7
|
+
}
|
|
8
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Claude Code Measurement Marker Check
|
|
2
|
+
|
|
3
|
+
实战二独立技能包:装修量尺图缺标记检查。
|
|
4
|
+
|
|
5
|
+
P0 小闭环:
|
|
6
|
+
|
|
7
|
+
1. 用户上传一张量尺图、手绘量尺单、户型草图或现场测量截图。
|
|
8
|
+
2. Claude Code 先观察图片,提取已标记内容、疑似缺标位置和模糊待确认区域。
|
|
9
|
+
3. 本地工具把结构化观察转成稳定报告:缺标记清单、风险说明、图像依据、人工复核建议。
|
|
10
|
+
|
|
11
|
+
## 用户入口话术
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
帮我检查这张装修量尺图有没有哪里未标记。
|
|
15
|
+
重点看空间名称、门窗洞口、长宽高、管道/插座/烟道和现场限制备注。
|
|
16
|
+
先给我缺标记清单和人工复核建议。
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 本地验证
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm --prefix claude-code-measurement-marker-check run measure:sample
|
|
23
|
+
npm --prefix claude-code-measurement-marker-check run smoke
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Claude Code 加载
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
claude --plugin-dir ./claude-code-measurement-marker-check
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
进入 Claude Code 后可直接调用:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
/measurement-marker-check:measurement-marker-check
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 参考实现
|
|
39
|
+
|
|
40
|
+
参考项目 `E:\ltc-pobingfeng` 中的图片识别调用方式:
|
|
41
|
+
|
|
42
|
+
- `src/modules/owner/nav/consultation/menchuang-agent/image-analysis.service.ts`
|
|
43
|
+
- `src/modules/owner/nav/consultation/menchuang-agent/window-recognition.service.ts`
|
|
44
|
+
- `AI模型导入与使用文档.md`
|
|
45
|
+
|
|
46
|
+
核心模式是 `completionJSON` 结构化输出,并通过 `vision: true` 和 `images: [imageUrl]` 传入图片。当前独立技能 P0 优先利用 Claude Code 的上传图片视觉能力完成观察,再用本地工具固化报告。
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
8
|
+
const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'measurement-marker-check');
|
|
9
|
+
const COPY_ENTRIES = [
|
|
10
|
+
'.claude-plugin',
|
|
11
|
+
'README.md',
|
|
12
|
+
'scripts',
|
|
13
|
+
'skill-package-manifest.json',
|
|
14
|
+
'skills',
|
|
15
|
+
'tools',
|
|
16
|
+
'package.json'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = { command: argv[0] || 'help', target: DEFAULT_TARGET, smoke: false };
|
|
21
|
+
for (let i = 1; i < argv.length; i++) {
|
|
22
|
+
const token = argv[i];
|
|
23
|
+
if (token === '--target' && argv[i + 1]) {
|
|
24
|
+
args.target = path.resolve(argv[++i]);
|
|
25
|
+
} else if (token === '--smoke') {
|
|
26
|
+
args.smoke = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return args;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function printHelp() {
|
|
33
|
+
console.log([
|
|
34
|
+
'Claude 量尺图缺标记技能包安装工具',
|
|
35
|
+
'',
|
|
36
|
+
'Usage:',
|
|
37
|
+
' claude-measure-marker install [--target <dir>] [--smoke]',
|
|
38
|
+
' claude-measure-marker check',
|
|
39
|
+
' claude-measure-marker smoke',
|
|
40
|
+
' claude-measure-marker path',
|
|
41
|
+
'',
|
|
42
|
+
'Examples:',
|
|
43
|
+
' npx @gangvy/claude-code-measurement-marker-check install',
|
|
44
|
+
' claude --plugin-dir "%USERPROFILE%\\.claude\\plugins\\measurement-marker-check"'
|
|
45
|
+
].join('\n'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function removeTarget(target) {
|
|
49
|
+
if (!target.includes(path.join(os.homedir(), '.claude', 'plugins')) && fs.existsSync(target)) {
|
|
50
|
+
throw new Error(`Refusing to overwrite non-Claude plugin directory: ${target}`);
|
|
51
|
+
}
|
|
52
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function copyPlugin(target) {
|
|
56
|
+
removeTarget(target);
|
|
57
|
+
fs.mkdirSync(target, { recursive: true });
|
|
58
|
+
|
|
59
|
+
for (const entry of COPY_ENTRIES) {
|
|
60
|
+
const source = path.join(ROOT, entry);
|
|
61
|
+
if (!fs.existsSync(source)) continue;
|
|
62
|
+
const destination = path.join(target, entry);
|
|
63
|
+
fs.cpSync(source, destination, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function checkPlugin(target) {
|
|
68
|
+
const required = [
|
|
69
|
+
'.claude-plugin/plugin.json',
|
|
70
|
+
'skills/measurement-marker-check/SKILL.md',
|
|
71
|
+
'tools/measurement-marker-check-run.js',
|
|
72
|
+
'package.json'
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const missing = required.filter(entry => !fs.existsSync(path.join(target, entry)));
|
|
76
|
+
if (missing.length) {
|
|
77
|
+
throw new Error(`安装目录缺少文件:${missing.join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
status: 'ok',
|
|
82
|
+
target,
|
|
83
|
+
required
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function runSmoke(target) {
|
|
88
|
+
const result = spawnSync(process.execPath, ['scripts/smoke.js'], {
|
|
89
|
+
cwd: target,
|
|
90
|
+
stdio: 'inherit',
|
|
91
|
+
shell: false
|
|
92
|
+
});
|
|
93
|
+
if (result.status !== 0) {
|
|
94
|
+
throw new Error('smoke failed');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function printNextSteps(target) {
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log('安装完成。下一步:');
|
|
101
|
+
console.log(` claude --plugin-dir "${target}"`);
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log('进入 Claude Code 后可说:');
|
|
104
|
+
console.log(' 帮我检查这张装修量尺图有没有哪里未标记。');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function main() {
|
|
108
|
+
const args = parseArgs(process.argv.slice(2));
|
|
109
|
+
|
|
110
|
+
if (args.command === 'help' || args.command === '--help' || args.command === '-h') {
|
|
111
|
+
printHelp();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (args.command === 'path') {
|
|
116
|
+
console.log(args.target);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (args.command === 'install') {
|
|
121
|
+
copyPlugin(args.target);
|
|
122
|
+
const check = checkPlugin(args.target);
|
|
123
|
+
console.log(JSON.stringify(check, null, 2));
|
|
124
|
+
if (args.smoke) runSmoke(args.target);
|
|
125
|
+
printNextSteps(args.target);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (args.command === 'check') {
|
|
130
|
+
console.log(JSON.stringify(checkPlugin(args.target), null, 2));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (args.command === 'smoke') {
|
|
135
|
+
runSmoke(ROOT);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
printHelp();
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
main();
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(`claude-measure-marker 执行失败:${error.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gangvy/claude-code-measurement-marker-check",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code skill for P0 renovation measurement drawing missing-marker checks.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-measure-marker": "bin/claude-measure-marker.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"measure:sample": "node tools/measurement-marker-check-run.js --sample --output ../outputs/claude-code-measurement-marker-sample --result-prefix MEASURE_MARKER_RESULT",
|
|
11
|
+
"smoke": "node scripts/smoke.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
".claude-plugin/",
|
|
15
|
+
"bin/",
|
|
16
|
+
"README.md",
|
|
17
|
+
"scripts/",
|
|
18
|
+
"skill-package-manifest.json",
|
|
19
|
+
"skills/",
|
|
20
|
+
"tools/"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {}
|
|
23
|
+
}
|
package/scripts/smoke.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { runMeasurementMarkerCheck } = require('../tools/measurement-marker-check-run');
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const output = path.resolve(__dirname, '..', 'outputs', 'measurement-marker-smoke');
|
|
7
|
+
const sample = await runMeasurementMarkerCheck({
|
|
8
|
+
sample: true,
|
|
9
|
+
output
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (sample.status !== 'ok') {
|
|
13
|
+
throw new Error(`sample failed: ${sample.status}`);
|
|
14
|
+
}
|
|
15
|
+
if (!sample.summary || sample.summary.missingCount < 1) {
|
|
16
|
+
throw new Error('sample did not report missing markers');
|
|
17
|
+
}
|
|
18
|
+
if (!Array.isArray(sample.files) || sample.files.length < 2) {
|
|
19
|
+
throw new Error('sample did not write report files');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const needsVision = await runMeasurementMarkerCheck({
|
|
23
|
+
image: 'demo-measurement.jpg'
|
|
24
|
+
});
|
|
25
|
+
if (needsVision.status !== 'needs_vision_observation') {
|
|
26
|
+
throw new Error(`expected needs_vision_observation, got: ${needsVision.status}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(JSON.stringify({
|
|
30
|
+
status: 'ok',
|
|
31
|
+
checks: [
|
|
32
|
+
'sample report',
|
|
33
|
+
'missing marker count',
|
|
34
|
+
'report files',
|
|
35
|
+
'needs vision observation guard'
|
|
36
|
+
],
|
|
37
|
+
missingCount: sample.summary.missingCount,
|
|
38
|
+
uncertainCount: sample.summary.uncertainCount,
|
|
39
|
+
files: sample.files
|
|
40
|
+
}, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch(error => {
|
|
44
|
+
console.error(JSON.stringify({
|
|
45
|
+
status: 'error',
|
|
46
|
+
message: error.message
|
|
47
|
+
}, null, 2));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-code-measurement-marker-check",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code 独立技能包:装修量尺图缺标记检查。P0 支持上传量尺图后识别疑似未标记位置、待复核区域和补标建议。",
|
|
5
|
+
"plugin": "measurement-marker-check",
|
|
6
|
+
"skills": [
|
|
7
|
+
"measurement-marker-check"
|
|
8
|
+
],
|
|
9
|
+
"entrySkill": "measurement-marker-check",
|
|
10
|
+
"sampleCommand": "npm run measure:sample",
|
|
11
|
+
"smokeCommand": "npm run smoke",
|
|
12
|
+
"installHint": "独立加载此目录即可:claude --plugin-dir ./claude-code-measurement-marker-check"
|
|
13
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: measurement-marker-check
|
|
3
|
+
description: Check装修量尺图、手绘量尺单、户型草图或现场测量图片中是否存在疑似未标记位置,输出缺标记清单、待复核区域和下一步补标建议。Use when the user asks for 实战二、装修量尺、量尺图识别、尺寸图缺标记、哪里没标、未标注检查 or measurement drawing review.
|
|
4
|
+
allowed-tools: Read Write Bash(node *)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 装修量尺图缺标记检查官
|
|
8
|
+
|
|
9
|
+
## 角色
|
|
10
|
+
|
|
11
|
+
你是面向设计/量尺团队的“量尺图缺标记检查官”。你的任务是看懂用户上传的量尺图、手绘草图、现场测量照片或截图,判断图中是否存在疑似未标记的位置,并把结果转成可复核的业务清单。
|
|
12
|
+
|
|
13
|
+
P0 目标不是一次性替代人工量尺,而是跑通一个小闭环:
|
|
14
|
+
|
|
15
|
+
- 用户上传一张量尺图。
|
|
16
|
+
- 你识别已标记内容和疑似缺标位置。
|
|
17
|
+
- 你输出“哪里可能没标、缺什么、为什么有风险、下一步怎么补”。
|
|
18
|
+
- 结果要能被设计师或量尺人员现场确认。
|
|
19
|
+
|
|
20
|
+
## 触发场景
|
|
21
|
+
|
|
22
|
+
当用户提到以下需求时,使用本技能:
|
|
23
|
+
|
|
24
|
+
- “实战二量尺图识别”
|
|
25
|
+
- “上传量尺图看哪里没标”
|
|
26
|
+
- “检查这张尺寸图有没有漏标”
|
|
27
|
+
- “量尺图是否有未标记”
|
|
28
|
+
- “手写量尺单识别缺失项”
|
|
29
|
+
- “Claudecode 流程跑实战2”
|
|
30
|
+
|
|
31
|
+
## 看图检查口径
|
|
32
|
+
|
|
33
|
+
默认检查以下标记是否清楚:
|
|
34
|
+
|
|
35
|
+
- 空间名称:客厅、卧室、厨房、卫生间、阳台等。
|
|
36
|
+
- 关键尺寸:长、宽、高、门洞、窗洞、墙面、柜体位置等。
|
|
37
|
+
- 门窗洞口:宽高、离地高度、开启方向、门套或窗台限制。
|
|
38
|
+
- 梁柱/墙体限制:梁高、柱位、墙厚、承重/非承重提示。
|
|
39
|
+
- 水电/管道/烟道:下水、地漏、插座、开关、燃气、烟道、排水管。
|
|
40
|
+
- 安装避让备注:需避让、不可拆、需复核、现场限制、手写备注。
|
|
41
|
+
|
|
42
|
+
## 参考识图实现
|
|
43
|
+
|
|
44
|
+
参考 `E:\ltc-pobingfeng` 的图片识别功能,尤其是:
|
|
45
|
+
|
|
46
|
+
- `AI模型导入与使用文档.md`
|
|
47
|
+
- `src/modules/owner/nav/consultation/menchuang-agent/image-analysis.service.ts`
|
|
48
|
+
- `src/modules/owner/nav/consultation/menchuang-agent/window-recognition.service.ts`
|
|
49
|
+
|
|
50
|
+
该项目的关键做法是:用 `completionJSON` 定义 JSON 输出格式,并在调用参数中设置 `vision: true`、`images: [imageUrl]`。本技能 P0 在 Claude Code 里先利用上传图片的视觉能力完成观察,再调用本技能目录里的本地工具固化报告。
|
|
51
|
+
|
|
52
|
+
## 工作流
|
|
53
|
+
|
|
54
|
+
1. 如果用户没有给图片或图片路径,先让用户上传量尺图,不要生成空报告。
|
|
55
|
+
2. 如果用户上传了图片或给了本地图片路径,先直接观察图片内容。
|
|
56
|
+
3. 输出前先在内部整理结构化观察:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"project": "项目或样本名称",
|
|
61
|
+
"image": "图片文件名或路径",
|
|
62
|
+
"detectedMarkers": [
|
|
63
|
+
{
|
|
64
|
+
"area": "厨房右侧墙面",
|
|
65
|
+
"markerType": "关键尺寸",
|
|
66
|
+
"value": "3200mm",
|
|
67
|
+
"evidence": "墙面横向尺寸已标注",
|
|
68
|
+
"confidence": 0.9
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"missingMarkers": [
|
|
72
|
+
{
|
|
73
|
+
"area": "窗户下沿",
|
|
74
|
+
"missingType": "窗台高度",
|
|
75
|
+
"risk": "窗台高度缺失,可能影响台面、吊柜或窗帘盒设计",
|
|
76
|
+
"evidence": "窗户位置已画出,但没有离地高度标注",
|
|
77
|
+
"confidence": 0.78
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"uncertainAreas": [
|
|
81
|
+
{
|
|
82
|
+
"area": "厨房右上角",
|
|
83
|
+
"reason": "疑似烟道或立管,但图中文字不清晰",
|
|
84
|
+
"action": "请现场照片或原始量尺单复核",
|
|
85
|
+
"confidence": 0.58
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"overallJudgement": "存在关键缺标记,建议人工复核后再进入标准尺寸表。"
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
4. 把上述 JSON 写入工作区临时文件,例如 `outputs/measurement-marker-check/input.json`。
|
|
93
|
+
5. 调用本地工具生成稳定报告:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
node "${CLAUDE_SKILL_DIR}/../../tools/measurement-marker-check-run.js" --input "outputs/measurement-marker-check/input.json" --output "outputs/measurement-marker-check" --result-prefix MEASURE_MARKER_RESULT
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
6. 优先把工具返回的 `assistantMessage` 正文发给用户。文件路径只放在末尾作为归档信息。
|
|
100
|
+
|
|
101
|
+
P0 sample 演示命令:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
node "${CLAUDE_SKILL_DIR}/../../tools/measurement-marker-check-run.js" --sample --output "outputs/measurement-marker-check-sample" --result-prefix MEASURE_MARKER_RESULT
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 输出规则
|
|
108
|
+
|
|
109
|
+
- 先给结论:是否存在疑似缺标记。
|
|
110
|
+
- 必须明确“区域 + 缺失类型 + 风险 + 图像依据 + 置信度”。
|
|
111
|
+
- 对看不清的地方要归入“待人工复核区域”,不要强行判定。
|
|
112
|
+
- 如果没有发现明显缺标,也要说明检查口径和残余风险。
|
|
113
|
+
- 不要只说“图片不清晰”;要指出哪里不清晰、需要补什么资料。
|
|
114
|
+
- 不要把 P0 结果说成最终工程结论;它是辅助复核清单。
|
|
115
|
+
- 不要暴露模型参数、接口参数或工具实现细节给业务用户。
|
|
116
|
+
|
|
117
|
+
## 推荐启动话术
|
|
118
|
+
|
|
119
|
+
用户可以直接说:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
帮我检查这张装修量尺图有没有哪里未标记。
|
|
123
|
+
重点看空间名称、门窗洞口、长宽高、管道/插座/烟道和现场限制备注。
|
|
124
|
+
先给我缺标记清单和人工复核建议。
|
|
125
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Fmode 图片识别参考
|
|
2
|
+
|
|
3
|
+
参考项目:`E:\ltc-pobingfeng`
|
|
4
|
+
|
|
5
|
+
关键文件:
|
|
6
|
+
|
|
7
|
+
- `AI模型导入与使用文档.md`
|
|
8
|
+
- `src/modules/owner/nav/consultation/menchuang-agent/image-analysis.service.ts`
|
|
9
|
+
- `src/modules/owner/nav/consultation/menchuang-agent/window-recognition.service.ts`
|
|
10
|
+
|
|
11
|
+
## 核心调用方式
|
|
12
|
+
|
|
13
|
+
参考项目使用 `fmode-ng/core` 的 `completionJSON` 做结构化 JSON 输出:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { completionJSON } from 'fmode-ng/core';
|
|
17
|
+
|
|
18
|
+
const result = await completionJSON(
|
|
19
|
+
systemPrompt,
|
|
20
|
+
outputFormat,
|
|
21
|
+
onProgress,
|
|
22
|
+
3,
|
|
23
|
+
{
|
|
24
|
+
model: 'fmode-1.6-cn',
|
|
25
|
+
temperature: 0.1,
|
|
26
|
+
vision: true,
|
|
27
|
+
images: [imageUrl]
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 对实战二的迁移方式
|
|
33
|
+
|
|
34
|
+
P0 阶段在 Claude Code 技能里先不引入前端上传链路,流程拆为两段:
|
|
35
|
+
|
|
36
|
+
1. Claude Code 接收用户上传的量尺图,并做视觉观察。
|
|
37
|
+
2. 本地工具接收结构化观察结果,生成稳定的缺标记报告。
|
|
38
|
+
|
|
39
|
+
后续如果要接入前端,可复用参考项目的上传和图片 URL 识别方式:
|
|
40
|
+
|
|
41
|
+
- 图片上传到可访问 URL。
|
|
42
|
+
- 用 `completionJSON` 加 `vision: true` 和 `images: [imageUrl]` 调模型。
|
|
43
|
+
- 输出固定 JSON,再交给本技能的报告工具生成业务可读结果。
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_REQUIRED_MARKERS = [
|
|
6
|
+
'空间名称',
|
|
7
|
+
'关键尺寸',
|
|
8
|
+
'门窗洞口',
|
|
9
|
+
'梁柱/墙体限制',
|
|
10
|
+
'水电/管道/烟道',
|
|
11
|
+
'安装避让备注'
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const SAMPLE_ANALYSIS = {
|
|
15
|
+
project: '实战二 P0 演示样本',
|
|
16
|
+
image: 'sample-measurement-plan.jpg',
|
|
17
|
+
sourceType: 'image',
|
|
18
|
+
detectedMarkers: [
|
|
19
|
+
{
|
|
20
|
+
area: '厨房右侧墙面',
|
|
21
|
+
markerType: '关键尺寸',
|
|
22
|
+
value: '3200mm',
|
|
23
|
+
evidence: '墙面横向尺寸已标注',
|
|
24
|
+
confidence: 0.9
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
area: '厨房水槽区域',
|
|
28
|
+
markerType: '水电/管道/烟道',
|
|
29
|
+
value: '下水管',
|
|
30
|
+
evidence: '右下角有管道符号和文字',
|
|
31
|
+
confidence: 0.82
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
missingMarkers: [
|
|
35
|
+
{
|
|
36
|
+
area: '厨房左侧门洞',
|
|
37
|
+
missingType: '门洞宽度/高度',
|
|
38
|
+
risk: '门洞尺寸缺失,后续柜体和动线复核容易出现偏差',
|
|
39
|
+
evidence: '图中能看到门洞轮廓,但附近没有宽高数值',
|
|
40
|
+
confidence: 0.86
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
area: '窗户下沿',
|
|
44
|
+
missingType: '窗台高度',
|
|
45
|
+
risk: '窗台高度缺失,可能影响台面、吊柜或窗帘盒设计',
|
|
46
|
+
evidence: '窗户位置已画出,但没有离地高度标注',
|
|
47
|
+
confidence: 0.78
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
uncertainAreas: [
|
|
51
|
+
{
|
|
52
|
+
area: '厨房右上角',
|
|
53
|
+
reason: '疑似烟道或立管,但图中文字不清晰',
|
|
54
|
+
action: '请现场照片或原始量尺单复核',
|
|
55
|
+
confidence: 0.58
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
overallJudgement: '存在关键缺标记,建议人工复核后再进入标准尺寸表。'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function parseArgs(argv) {
|
|
62
|
+
const args = {};
|
|
63
|
+
for (let i = 0; i < argv.length; i++) {
|
|
64
|
+
const token = argv[i];
|
|
65
|
+
if (!token.startsWith('--')) continue;
|
|
66
|
+
const eq = token.indexOf('=');
|
|
67
|
+
if (eq >= 0) {
|
|
68
|
+
args[token.slice(2, eq)] = token.slice(eq + 1);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const key = token.slice(2);
|
|
72
|
+
const next = argv[i + 1];
|
|
73
|
+
if (next && !next.startsWith('--')) {
|
|
74
|
+
args[key] = next;
|
|
75
|
+
i++;
|
|
76
|
+
} else {
|
|
77
|
+
args[key] = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return args;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function splitList(value) {
|
|
84
|
+
if (!value) return [];
|
|
85
|
+
if (Array.isArray(value)) return value.map(String).map(item => item.trim()).filter(Boolean);
|
|
86
|
+
return String(value).split(/[,,、\n\r]+/).map(item => item.trim()).filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readJsonFile(filePath) {
|
|
90
|
+
if (!filePath) return {};
|
|
91
|
+
const absolute = path.resolve(filePath);
|
|
92
|
+
if (!fs.existsSync(absolute)) {
|
|
93
|
+
throw new Error(`Input JSON not found: ${absolute}`);
|
|
94
|
+
}
|
|
95
|
+
return JSON.parse(fs.readFileSync(absolute, 'utf8'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseJsonValue(value, fallback) {
|
|
99
|
+
if (!value) return fallback;
|
|
100
|
+
if (Array.isArray(value) || typeof value === 'object') return value;
|
|
101
|
+
return JSON.parse(String(value));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function asArray(value) {
|
|
105
|
+
return Array.isArray(value) ? value : [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeInput(input = {}) {
|
|
109
|
+
const fromFile = readJsonFile(input.input || input['input-json']);
|
|
110
|
+
const detectedMarkers = parseJsonValue(input.detectedMarkers || input['detected-markers'], fromFile.detectedMarkers || []);
|
|
111
|
+
const missingMarkers = parseJsonValue(input.missingMarkers || input['missing-markers'], fromFile.missingMarkers || []);
|
|
112
|
+
const uncertainAreas = parseJsonValue(input.uncertainAreas || input['uncertain-areas'], fromFile.uncertainAreas || []);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
...fromFile,
|
|
116
|
+
...input,
|
|
117
|
+
sample: Boolean(input.sample || fromFile.sample),
|
|
118
|
+
project: input.project || fromFile.project,
|
|
119
|
+
image: input.image || input.imageUrl || input.imagePath || fromFile.image || fromFile.imageUrl || fromFile.imagePath,
|
|
120
|
+
imageUrl: input.imageUrl || input['image-url'] || fromFile.imageUrl,
|
|
121
|
+
imagePath: input.imagePath || input['image-path'] || fromFile.imagePath,
|
|
122
|
+
sourceType: input.sourceType || input['source-type'] || fromFile.sourceType,
|
|
123
|
+
requiredMarkers: splitList(input.requiredMarkers || input['required-markers']).length
|
|
124
|
+
? splitList(input.requiredMarkers || input['required-markers'])
|
|
125
|
+
: fromFile.requiredMarkers,
|
|
126
|
+
detectedMarkers: asArray(detectedMarkers),
|
|
127
|
+
missingMarkers: asArray(missingMarkers),
|
|
128
|
+
uncertainAreas: asArray(uncertainAreas),
|
|
129
|
+
overallJudgement: input.overallJudgement || input['overall-judgement'] || fromFile.overallJudgement
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function okResult(payload = {}) {
|
|
134
|
+
return {
|
|
135
|
+
status: 'ok',
|
|
136
|
+
assistantMessage: payload.assistantMessage || '',
|
|
137
|
+
summary: payload.summary || {},
|
|
138
|
+
data: payload.data || {},
|
|
139
|
+
files: payload.files || [],
|
|
140
|
+
nextActions: payload.nextActions || [],
|
|
141
|
+
warnings: payload.warnings || [],
|
|
142
|
+
errors: payload.errors || []
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function errorResult(message, payload = {}) {
|
|
147
|
+
return {
|
|
148
|
+
status: 'error',
|
|
149
|
+
assistantMessage: message,
|
|
150
|
+
summary: payload.summary || {},
|
|
151
|
+
data: payload.data || {},
|
|
152
|
+
files: payload.files || [],
|
|
153
|
+
nextActions: payload.nextActions || [],
|
|
154
|
+
warnings: payload.warnings || [],
|
|
155
|
+
errors: payload.errors || [{ message }]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeConfidence(value) {
|
|
160
|
+
if (typeof value !== 'number' || Number.isNaN(value)) return 0.7;
|
|
161
|
+
return Math.max(0, Math.min(1, value));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeMarker(item = {}) {
|
|
165
|
+
return {
|
|
166
|
+
area: item.area || item.location || item.space || '未指明区域',
|
|
167
|
+
markerType: item.markerType || item.type || item.label || '未分类标记',
|
|
168
|
+
value: item.value || item.text || '',
|
|
169
|
+
evidence: item.evidence || item.reason || '',
|
|
170
|
+
confidence: normalizeConfidence(item.confidence)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeMissing(item = {}) {
|
|
175
|
+
return {
|
|
176
|
+
area: item.area || item.location || item.space || '未指明区域',
|
|
177
|
+
missingType: item.missingType || item.markerType || item.type || '未说明缺失项',
|
|
178
|
+
risk: item.risk || item.impact || '需要人工复核,避免后续尺寸整理漏项。',
|
|
179
|
+
evidence: item.evidence || item.reason || '',
|
|
180
|
+
confidence: normalizeConfidence(item.confidence)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeUncertain(item = {}) {
|
|
185
|
+
return {
|
|
186
|
+
area: item.area || item.location || item.space || '未指明区域',
|
|
187
|
+
reason: item.reason || item.evidence || '图像信息不足,暂无法确认。',
|
|
188
|
+
action: item.action || '建议回看原始量尺图或现场照片后复核。',
|
|
189
|
+
confidence: normalizeConfidence(item.confidence)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function countByType(items) {
|
|
194
|
+
return items.reduce((acc, item) => {
|
|
195
|
+
const key = item.markerType || item.missingType || '未分类';
|
|
196
|
+
acc[key] = (acc[key] || 0) + 1;
|
|
197
|
+
return acc;
|
|
198
|
+
}, {});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildMeasurementMarkerReport(input = {}) {
|
|
202
|
+
const source = input.sample ? SAMPLE_ANALYSIS : input;
|
|
203
|
+
const detectedMarkers = asArray(source.detectedMarkers).map(normalizeMarker);
|
|
204
|
+
const missingMarkers = asArray(source.missingMarkers).map(normalizeMissing);
|
|
205
|
+
const uncertainAreas = asArray(source.uncertainAreas).map(normalizeUncertain);
|
|
206
|
+
const requiredMarkers = asArray(source.requiredMarkers).length
|
|
207
|
+
? source.requiredMarkers.map(String)
|
|
208
|
+
: DEFAULT_REQUIRED_MARKERS;
|
|
209
|
+
const status = missingMarkers.length ? 'needs_review' : 'ok';
|
|
210
|
+
const pass = status === 'ok' && uncertainAreas.length === 0;
|
|
211
|
+
|
|
212
|
+
const summary = {
|
|
213
|
+
project: source.project || '装修量尺图缺标记检查',
|
|
214
|
+
image: source.image || source.imageUrl || source.imagePath || '',
|
|
215
|
+
sourceType: source.sourceType || 'image',
|
|
216
|
+
pass,
|
|
217
|
+
status,
|
|
218
|
+
detectedCount: detectedMarkers.length,
|
|
219
|
+
missingCount: missingMarkers.length,
|
|
220
|
+
uncertainCount: uncertainAreas.length,
|
|
221
|
+
detectedByType: countByType(detectedMarkers),
|
|
222
|
+
missingByType: countByType(missingMarkers)
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const assistantMessage = renderAssistantMessage({
|
|
226
|
+
summary,
|
|
227
|
+
requiredMarkers,
|
|
228
|
+
detectedMarkers,
|
|
229
|
+
missingMarkers,
|
|
230
|
+
uncertainAreas,
|
|
231
|
+
overallJudgement: source.overallJudgement
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
summary,
|
|
236
|
+
data: {
|
|
237
|
+
requiredMarkers,
|
|
238
|
+
detectedMarkers,
|
|
239
|
+
missingMarkers,
|
|
240
|
+
uncertainAreas,
|
|
241
|
+
overallJudgement: source.overallJudgement || defaultJudgement(summary)
|
|
242
|
+
},
|
|
243
|
+
assistantMessage
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function defaultJudgement(summary) {
|
|
248
|
+
if (summary.missingCount > 0) {
|
|
249
|
+
return '当前量尺图存在疑似未标记位置,建议先人工复核缺失项,再输出标准化尺寸表。';
|
|
250
|
+
}
|
|
251
|
+
if (summary.uncertainCount > 0) {
|
|
252
|
+
return '当前未发现明确缺标,但仍有图像不清晰区域,需要人工确认。';
|
|
253
|
+
}
|
|
254
|
+
return '当前未发现明显缺标记,可进入下一步标准化整理。';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderAssistantMessage({ summary, requiredMarkers, detectedMarkers, missingMarkers, uncertainAreas, overallJudgement }) {
|
|
258
|
+
const lines = [];
|
|
259
|
+
lines.push('## 量尺图缺标记检查(P0)');
|
|
260
|
+
lines.push('');
|
|
261
|
+
lines.push(`**结论**:${overallJudgement || defaultJudgement(summary)}`);
|
|
262
|
+
lines.push('');
|
|
263
|
+
lines.push(`- 已识别标记:${summary.detectedCount} 项`);
|
|
264
|
+
lines.push(`- 疑似缺标记:${summary.missingCount} 项`);
|
|
265
|
+
lines.push(`- 待确认区域:${summary.uncertainCount} 项`);
|
|
266
|
+
lines.push('');
|
|
267
|
+
lines.push('### 本轮检查口径');
|
|
268
|
+
requiredMarkers.forEach(item => lines.push(`- ${item}`));
|
|
269
|
+
lines.push('');
|
|
270
|
+
|
|
271
|
+
lines.push('### 疑似未标记位置');
|
|
272
|
+
if (missingMarkers.length) {
|
|
273
|
+
missingMarkers.forEach((item, index) => {
|
|
274
|
+
lines.push(`${index + 1}. ${item.area}:缺少「${item.missingType}」`);
|
|
275
|
+
lines.push(` - 风险:${item.risk}`);
|
|
276
|
+
if (item.evidence) lines.push(` - 图像依据:${item.evidence}`);
|
|
277
|
+
lines.push(` - 置信度:${Math.round(item.confidence * 100)}%`);
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
lines.push('- 暂未发现明确缺标记。');
|
|
281
|
+
}
|
|
282
|
+
lines.push('');
|
|
283
|
+
|
|
284
|
+
if (uncertainAreas.length) {
|
|
285
|
+
lines.push('### 待人工复核区域');
|
|
286
|
+
uncertainAreas.forEach((item, index) => {
|
|
287
|
+
lines.push(`${index + 1}. ${item.area}:${item.reason}`);
|
|
288
|
+
lines.push(` - 建议动作:${item.action}`);
|
|
289
|
+
lines.push(` - 置信度:${Math.round(item.confidence * 100)}%`);
|
|
290
|
+
});
|
|
291
|
+
lines.push('');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (detectedMarkers.length) {
|
|
295
|
+
lines.push('### 已识别标记摘录');
|
|
296
|
+
detectedMarkers.slice(0, 8).forEach((item, index) => {
|
|
297
|
+
const value = item.value ? `:${item.value}` : '';
|
|
298
|
+
const evidence = item.evidence ? `(${item.evidence})` : '';
|
|
299
|
+
lines.push(`${index + 1}. ${item.area} / ${item.markerType}${value}${evidence}`);
|
|
300
|
+
});
|
|
301
|
+
if (detectedMarkers.length > 8) {
|
|
302
|
+
lines.push(`- 另有 ${detectedMarkers.length - 8} 项已识别标记写入结构化结果。`);
|
|
303
|
+
}
|
|
304
|
+
lines.push('');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
lines.push('### 下一步');
|
|
308
|
+
if (missingMarkers.length || uncertainAreas.length) {
|
|
309
|
+
lines.push('- 先让量尺/设计同事确认上述缺标或模糊区域。');
|
|
310
|
+
lines.push('- 复核后补齐尺寸、限制条件或备注,再生成标准化尺寸对照表。');
|
|
311
|
+
} else {
|
|
312
|
+
lines.push('- 可继续输出标准化尺寸对照表和异常预警清单。');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return lines.join('\n');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function writeMeasurementMarkerReport(outputDir, report) {
|
|
319
|
+
const absolute = path.resolve(outputDir || path.join('outputs', 'measurement-marker-check', new Date().toISOString().slice(0, 10)));
|
|
320
|
+
fs.mkdirSync(absolute, { recursive: true });
|
|
321
|
+
const jsonPath = path.join(absolute, 'measurement-marker-check-result.json');
|
|
322
|
+
const mdPath = path.join(absolute, 'measurement-marker-check-report.md');
|
|
323
|
+
fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf8');
|
|
324
|
+
fs.writeFileSync(mdPath, report.assistantMessage, 'utf8');
|
|
325
|
+
return [jsonPath, mdPath];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function runMeasurementMarkerCheck(input = {}) {
|
|
329
|
+
const normalized = normalizeInput(input);
|
|
330
|
+
const outputDir = path.resolve(normalized.output || path.join('outputs', 'measurement-marker-check', new Date().toISOString().slice(0, 10)));
|
|
331
|
+
|
|
332
|
+
if (!normalized.sample && !normalized.image && !normalized.imageUrl && !normalized.imagePath) {
|
|
333
|
+
return errorResult('请提供量尺图片路径/URL,或使用 --sample 跑 P0 演示样例。', {
|
|
334
|
+
nextActions: [
|
|
335
|
+
'上传或传入一张量尺图',
|
|
336
|
+
'Claude 先基于图片输出 detectedMarkers / missingMarkers / uncertainAreas',
|
|
337
|
+
'再调用本工具生成 P0 缺标记报告'
|
|
338
|
+
]
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!normalized.sample && !normalized.detectedMarkers.length && !normalized.missingMarkers.length && !normalized.uncertainAreas.length) {
|
|
343
|
+
return {
|
|
344
|
+
status: 'needs_vision_observation',
|
|
345
|
+
assistantMessage: [
|
|
346
|
+
'已收到量尺图片入口,但还缺少看图后的结构化观察。',
|
|
347
|
+
'',
|
|
348
|
+
'请先让 Claude 直接观察上传的量尺图,提取:',
|
|
349
|
+
'- detectedMarkers:已标记的空间、尺寸、门窗、管道、梁柱、备注等',
|
|
350
|
+
'- missingMarkers:疑似未标记的位置、缺失类型、风险、图像依据、置信度',
|
|
351
|
+
'- uncertainAreas:图像模糊或无法确定的位置',
|
|
352
|
+
'',
|
|
353
|
+
'随后把这些结构化结果传给本工具,即可生成 P0 缺标记报告。'
|
|
354
|
+
].join('\n'),
|
|
355
|
+
summary: {
|
|
356
|
+
image: normalized.image || normalized.imageUrl || normalized.imagePath,
|
|
357
|
+
needsVisionObservation: true
|
|
358
|
+
},
|
|
359
|
+
data: {
|
|
360
|
+
expectedInputShape: {
|
|
361
|
+
detectedMarkers: [{ area: '厨房右侧墙面', markerType: '关键尺寸', value: '3200mm', evidence: '墙面横向尺寸已标注', confidence: 0.9 }],
|
|
362
|
+
missingMarkers: [{ area: '窗户下沿', missingType: '窗台高度', risk: '影响台面或窗帘盒设计', evidence: '窗户有轮廓但未见离地高度', confidence: 0.78 }],
|
|
363
|
+
uncertainAreas: [{ area: '右上角', reason: '文字模糊', action: '人工复核原图', confidence: 0.58 }]
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
files: [],
|
|
367
|
+
nextActions: ['先进行图片视觉识别', '再调用 measurement marker check 工具生成报告'],
|
|
368
|
+
warnings: ['P0 阶段不在本地工具内直接调用视觉模型,由 Claude Code 视觉能力或上游识图服务提供观察结果。'],
|
|
369
|
+
errors: []
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const report = buildMeasurementMarkerReport(normalized);
|
|
374
|
+
const files = writeMeasurementMarkerReport(outputDir, report);
|
|
375
|
+
|
|
376
|
+
return okResult({
|
|
377
|
+
assistantMessage: report.assistantMessage,
|
|
378
|
+
summary: report.summary,
|
|
379
|
+
data: report.data,
|
|
380
|
+
files,
|
|
381
|
+
nextActions: report.summary.missingCount || report.summary.uncertainCount
|
|
382
|
+
? ['让量尺/设计同事补充缺标或模糊区域', '补齐后继续输出标准化尺寸对照表']
|
|
383
|
+
: ['继续输出标准化尺寸对照表', '可选:与人工标准表做差异对比'],
|
|
384
|
+
warnings: normalized.sample ? ['当前使用 P0 sample 演示数据。'] : []
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function main() {
|
|
389
|
+
const args = parseArgs(process.argv.slice(2));
|
|
390
|
+
try {
|
|
391
|
+
const result = await runMeasurementMarkerCheck(args);
|
|
392
|
+
console.log(JSON.stringify(result, null, 2));
|
|
393
|
+
if (args.resultPrefix || args['result-prefix']) {
|
|
394
|
+
const prefix = args.resultPrefix || args['result-prefix'];
|
|
395
|
+
console.log(`${prefix}=${JSON.stringify(result)}`);
|
|
396
|
+
}
|
|
397
|
+
process.exit(result.status === 'ok' || result.status === 'needs_vision_observation' ? 0 : 1);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
const result = errorResult(error.message || String(error));
|
|
400
|
+
console.log(JSON.stringify(result, null, 2));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (require.main === module) {
|
|
406
|
+
main();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = {
|
|
410
|
+
runMeasurementMarkerCheck,
|
|
411
|
+
buildMeasurementMarkerReport,
|
|
412
|
+
SAMPLE_ANALYSIS,
|
|
413
|
+
DEFAULT_REQUIRED_MARKERS
|
|
414
|
+
};
|