@seeed-studio/solution-cli 0.3.2 → 0.4.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/lib/doctor.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import fs from 'node:fs';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import { getPlatformInfo, getEngineBinDir, getEngineBinaryPath, getConfiguredEnginePath, getContentDir } from './platform.js';
6
- import { verifyEngineBinary } from './detector.js';
6
+ import { verifyEngineBinary, detectDesktopApp } from './detector.js';
7
7
  import { loadLedger, computeFileHash } from './ledger.js';
8
8
 
9
9
  export function runDoctor() {
@@ -166,6 +166,24 @@ export function runDoctor() {
166
166
  });
167
167
  }
168
168
 
169
+ // Check 8: Desktop App (GUI)
170
+ const app = detectDesktopApp();
171
+ if (app.detected) {
172
+ checks.push({
173
+ id: 'desktop_app',
174
+ status: 'ok',
175
+ title: 'SenseCraft Solution Desktop App (GUI)',
176
+ detail: `Detected at ${app.appPath || app.binPath} (v${app.version || 'installed'})`
177
+ });
178
+ } else {
179
+ checks.push({
180
+ id: 'desktop_app',
181
+ status: 'ok',
182
+ title: 'SenseCraft Solution Desktop App (GUI)',
183
+ detail: 'Not detected (optional). For visual dashboard & GUI workflows, download at https://sensecraft.seeed.cc/download'
184
+ });
185
+ }
186
+
169
187
  const overall = checks.some(c => c.status === 'fail')
170
188
  ? 'fail'
171
189
  : checks.some(c => c.status === 'warn')
package/lib/downloader.js CHANGED
@@ -281,3 +281,27 @@ export function pullSolutionContent(binPath, onProgress = () => {}) {
281
281
  return { success: false, warning: msg };
282
282
  }
283
283
  }
284
+
285
+ export function updateSolutionContent(binPath, onProgress = () => {}) {
286
+ const contentDir = path.join(getScHome(), 'content', 'solutions');
287
+ const isEmptyStore = !fs.existsSync(contentDir) || fs.readdirSync(contentDir).filter(f => !f.startsWith('.')).length === 0;
288
+
289
+ if (isEmptyStore) {
290
+ onProgress('Initial solution content store is empty; performing full initial pull...');
291
+ return pullSolutionContent(binPath, onProgress);
292
+ }
293
+
294
+ onProgress('Updating official solution definitions from CDN (sensecraft-engine content update)...');
295
+ try {
296
+ execFileSync(binPath, ['content', 'update'], {
297
+ encoding: 'utf8',
298
+ timeout: 120000,
299
+ stdio: ['ignore', 'pipe', 'pipe']
300
+ });
301
+ onProgress('Solution content successfully updated to latest.');
302
+ return { success: true };
303
+ } catch (err) {
304
+ onProgress('Notice: incremental update fell back to full pull.');
305
+ return pullSolutionContent(binPath, onProgress);
306
+ }
307
+ }
package/lib/gotchas.js ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @file gotchas.js
3
+ * Edge hardware & chip troubleshooting knowledge base for Solution CLI.
4
+ */
5
+
6
+ export const GOTCHAS_DATABASE = [
7
+ {
8
+ id: 'rk3588_npu_device_missing',
9
+ chip: 'rk3588',
10
+ title: 'RK3588/RK3576 NPU 设备节点缺失或无权访问 (/dev/rknn)',
11
+ keywords: ['rk3588', 'rk3576', 'rknn', '/dev/rknn', 'npu', 'permission denied', 'no such file'],
12
+ symptoms: '容器内运行推理时报错 "cannot open /dev/rknn" 或 "Permission denied"',
13
+ root_cause: '宿主机未加载 rknpu 内核模块,或者 Docker 容器启动时未正确挂载 /dev/rknn 设备节点及配置 udev 权限。',
14
+ solution: [
15
+ '1. 检查宿主机内核节点:ls -l /dev/rknn (若不存在,执行 sudo modprobe rknpu)',
16
+ '2. 赋予设备访问权限:sudo chmod 666 /dev/rknn',
17
+ '3. Docker 启动或 Compose 配置中必须显式透传设备:',
18
+ ' devices:\n - /dev/rknn:/dev/rknn\n - /dev/dma_heap:/dev/dma_heap',
19
+ '4. 检查驱动日志:dmesg | grep -i rknpu'
20
+ ],
21
+ forbidden: '不要在容器内尝试执行 apt install 重新安装原厂内核 BSP,底层驱动必须依赖宿主机内核。'
22
+ },
23
+ {
24
+ id: 'rk3588_docker_nftables_conflict',
25
+ chip: 'rk3588',
26
+ title: 'Rockchip Ubuntu Docker 网络互通死锁 (iptables-legacy 冲突)',
27
+ keywords: ['rk3588', 'rk3576', 'docker', 'iptables', 'nftables', 'network', 'bridge'],
28
+ symptoms: 'Docker 服务启动失败,或容器内无法通过桥接网关访问外部网络与外设流。',
29
+ root_cause: 'Rockchip 定制 Linux 6.1 内核对 nftables 的支持不完备,而较新版 Docker 默认尝试使用 iptables-nft。',
30
+ solution: [
31
+ '1. 切换系统默认 iptables 为 legacy 模式:',
32
+ ' sudo update-alternatives --set iptables /usr/sbin/iptables-legacy',
33
+ ' sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy',
34
+ '2. 重启 Docker 守护进程:sudo systemctl restart docker'
35
+ ],
36
+ forbidden: '切勿卸载 iptables 软件包或随意清空 iptables -F,这会导致 Docker 默认 NAT 转发链彻底损坏。'
37
+ },
38
+ {
39
+ id: 'recamera_vpss_overflow',
40
+ chip: 'recamera',
41
+ title: 'reCamera (SG2002 / RV1126B) 图像管道溢出 (VPSS FIFO overflow)',
42
+ keywords: ['recamera', 'vpss', 'fifo', 'overflow', 'sg2002', 'rv1126b', 'isp', 'vi'],
43
+ symptoms: '视频流卡死、黑屏,dmesg 频繁刷新 "VPSS FIFO overflow" 或 "VI drop frame"',
44
+ root_cause: '推理进程处理帧率低于摄像头输入帧率,导致硬件 ISP/VPSS 环形缓冲区被填满并阻塞硬件通道。',
45
+ solution: [
46
+ '1. 在 rkipc.ini 或流媒体配置中适当降低码流分辨率(从 1440P 降至 1080P 或 720P)',
47
+ '2. 限制推理帧率(如设置 skip_frames: 2,每秒处理 10~15 帧)',
48
+ '3. 检查模型加载是否启用了硬件 NPU 加速(.cvimodel / .rknn),避免 CPU 软解码'
49
+ ],
50
+ forbidden: '不要在视频流已卡死时直接硬重启机器,应先优雅杀掉流媒体服务以释放 VI/VPSS 通道。'
51
+ },
52
+ {
53
+ id: 'recamera_process_kill_safety',
54
+ chip: 'recamera',
55
+ title: 'reCamera 服务热重启安全规约:严禁 while true 且必须 sleep 3',
56
+ keywords: ['recamera', 'restart', 'fork bomb', 'sleep 3', 'pgrep', 'kill', 'watchdog'],
57
+ symptoms: '修改二进制或配置后设备网络全断,SSH/HTTP 无法连接,只能拔电硬重启。',
58
+ root_cause: '1. 硬件 VPSS/VI 资源被旧进程锁定未完全释放即启动新进程;2. 监控脚本使用死循环在二进制闪退时触发 Fork 风暴耗尽内核 PID。',
59
+ solution: [
60
+ '1. 优雅停止进程:for p in $(pgrep -f rkipc); do kill -9 $p; done',
61
+ '2. 关键安全延迟:必须等待 3 秒释放硬件通道:sleep 3',
62
+ '3. 安全拉起新服务:/userdata/rkipc.patch -c /userdata/rkipc.ini &'
63
+ ],
64
+ forbidden: '严禁在脚本中使用 while true; do /path/app; sleep 1; done 守护!一旦二进制参数错误快速退出,会导致系统瘫痪。'
65
+ },
66
+ {
67
+ id: 'jetson_nvidia_runtime_missing',
68
+ chip: 'jetson',
69
+ title: 'Jetson Orin (J4012/J2012) 容器缺失 NVIDIA Container Runtime',
70
+ keywords: ['jetson', 'orin', 'j4012', 'j4011', 'j2012', 'cuda', 'tensorrt', 'nvidia runtime'],
71
+ symptoms: 'Docker 容器启动报 "CUDA driver version is insufficient" 或无法找到 /dev/nvhost-* 设备',
72
+ root_cause: 'Docker 守护进程未配置默认 nvidia runtime,或运行 docker compose 时未指定 runtime: nvidia。',
73
+ solution: [
74
+ '1. 检查 /etc/docker/daemon.json 是否包含 "default-runtime": "nvidia"',
75
+ '2. 运行单个容器时添加参数:docker run --runtime nvidia ...',
76
+ '3. Compose 配置中加入:\n runtime: nvidia\n environment:\n - NVIDIA_VISIBLE_DEVICES=all\n - NVIDIA_DRIVER_CAPABILITIES=all'
77
+ ],
78
+ forbidden: '不要在 Jetson 容器内盲目安装标准 x86 的 NVIDIA 官方显卡驱动,Jetson 采用系统级统一内存 L4T BSP。'
79
+ },
80
+ {
81
+ id: 'hailo_driver_version_mismatch',
82
+ chip: 'hailo',
83
+ title: 'reComputer R2000 / RPi Hailo-8 驱动与运行库版本不匹配',
84
+ keywords: ['hailo', 'hailo8', 'libhailort', 'hailortcli', 'version mismatch', 'r2000'],
85
+ symptoms: '容器或应用启动报 "libhailort.so.4.21.0 not found" 或 "Driver version mismatch"',
86
+ root_cause: 'HailoRT 的内核驱动 (PCIe Driver)、用户态动态库 (libhailort) 和 GStreamer 插件版本必须严格完全一致(如均为 4.21.0)。',
87
+ solution: [
88
+ '1. 检查当前内核驱动状态:hailortcli fw-control identify',
89
+ '2. 检查动态库版本:ls -l /usr/lib/libhailort.so*',
90
+ '3. 确保宿主机与容器内镜像使用的 HailoRT 版本一致(官方套件均已固定为 4.21.0)'
91
+ ],
92
+ forbidden: '不要在已有应用正在占用 /dev/hailo0 时启动第二个推理容器,Hailo-8 默认排他独占访问。'
93
+ }
94
+ ];
95
+
96
+ /**
97
+ * Searches the gotchas troubleshooting database.
98
+ *
99
+ * @param {Object} options
100
+ * @param {string} [options.query] - Search term
101
+ * @param {string} [options.chip] - Target chip/platform (rk3588, recamera, jetson, hailo)
102
+ * @returns {Array<Object>} Matched gotchas records
103
+ */
104
+ export function searchGotchas({ query = '', chip = '' } = {}) {
105
+ const q = String(query || '').trim().toLowerCase();
106
+ const c = String(chip || '').trim().toLowerCase();
107
+
108
+ return GOTCHAS_DATABASE.filter((item) => {
109
+ if (c && !item.chip.toLowerCase().includes(c)) {
110
+ return false;
111
+ }
112
+ if (!q) return true;
113
+ const tokens = q.split(/\s+/).filter(Boolean);
114
+ const haystack = [
115
+ item.id,
116
+ item.chip,
117
+ item.title,
118
+ item.symptoms,
119
+ item.root_cause,
120
+ ...(item.keywords || []),
121
+ ...(Array.isArray(item.solution) ? item.solution : [item.solution]),
122
+ ].join(' ').toLowerCase();
123
+
124
+ return tokens.every((t) => haystack.includes(t));
125
+ });
126
+ }
package/lib/installer.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  getConfiguredEnginePath
10
10
  } from './platform.js';
11
11
  import { detectExistingEngine, detectDesktopApp, verifyEngineBinary, compareSemver } from './detector.js';
12
- import { downloadEngine, fetchManifest, validateReleaseManifest, pullSolutionContent } from './downloader.js';
12
+ import { downloadEngine, fetchManifest, validateReleaseManifest, pullSolutionContent, updateSolutionContent } from './downloader.js';
13
13
  import { getKnownAgents, scanLocalAgents, promptAgentSelection } from './agent.js';
14
14
  import {
15
15
  recordInstallation,
@@ -51,16 +51,18 @@ function assertCanonicalSkillState(ledger, agents) {
51
51
  export async function runInstall(options = {}) {
52
52
  const {
53
53
  agent = null,
54
+ skillsDir = null,
54
55
  yes = false,
55
56
  dryRun = false,
56
57
  skipContent = false,
57
58
  force = false,
58
59
  enginePath = null,
60
+ isUpdate = false,
59
61
  logger = console.log
60
62
  } = options;
61
63
 
62
64
  logger('\x1b[1m\x1b[34m====================================================\x1b[0m');
63
- logger('\x1b[1m\x1b[34m Solution CLI Installer \x1b[0m');
65
+ logger(`\x1b[1m\x1b[34m Solution CLI ${isUpdate ? 'Updater' : 'Installer'} \x1b[0m`);
64
66
  logger('\x1b[1m\x1b[34m====================================================\x1b[0m\n');
65
67
 
66
68
  const platform = getPlatformInfo();
@@ -70,10 +72,10 @@ export async function runInstall(options = {}) {
70
72
 
71
73
  // --- Step 0: Pre-validate agent targets before ANY download/side-effects (Codex Medium 11) ---
72
74
  const scanned = scanLocalAgents();
73
- const selectedAgents = await promptAgentSelection(scanned, agent, yes);
75
+ const selectedAgents = await promptAgentSelection(scanned, agent, yes, skillsDir);
74
76
 
75
77
  if (selectedAgents.length === 0) {
76
- logger('\nNo agents selected. Installation halted with no filesystem modifications.');
78
+ logger('\nNo agents selected. Operation halted with no filesystem modifications.');
77
79
  return { success: true, engineBin: null, installedAgents: [] };
78
80
  }
79
81
 
@@ -147,8 +149,13 @@ export async function runInstall(options = {}) {
147
149
 
148
150
  // --- Step 2: Content Cache ---
149
151
  if (!skipContent && !dryRun) {
150
- logger('\n🔄 Phase 2: Ensuring IoT solutions catalog is populated...');
151
- pullSolutionContent(engineBin, msg => logger(` ${msg}`));
152
+ if (isUpdate) {
153
+ logger('\n🔄 Phase 2: Updating IoT solutions catalog to latest...');
154
+ updateSolutionContent(engineBin, msg => logger(` ${msg}`));
155
+ } else {
156
+ logger('\n🔄 Phase 2: Ensuring IoT solutions catalog is populated...');
157
+ pullSolutionContent(engineBin, msg => logger(` ${msg}`));
158
+ }
152
159
  } else if (skipContent) {
153
160
  logger('\n🔄 Phase 2: Skipped solution pull (--skip-content).');
154
161
  }
@@ -243,9 +250,12 @@ export async function runInstall(options = {}) {
243
250
  logger(`\n🔧 Engine Binary: ${engineBin}`);
244
251
  logger(`🤖 Configured Agents:`);
245
252
  installedAgents.forEach(a => logger(` - ${a.name}: ${a.path}`));
246
- logger('\n👉 You can now open Work Buddy or your chosen Agent and ask:');
247
- logger(' "帮我把边缘安全方案 (edge_security) 部署到 Jetson (192.168.1.100)"');
248
- logger(' "查看当前有哪些可部署的物联网方案"');
253
+ logger('\n👉 [Instructions for AI Agent / 助手后续操作]:');
254
+ logger(' Solution CLI skill is now installed and activated.');
255
+ logger(' - Search IoT solutions: solution-cli hub search "<query>" --json');
256
+ logger(' - Check deployment specs: sensecraft-engine deploy-info <solution-id> --lang zh');
257
+ logger(' - Deploy (Local/Remote): sensecraft-engine deploy --params ./.sensecraft-work/body.json --yes');
258
+ logger(' Please inform the user that the skill is ready for solution selection and deployment.');
249
259
  logger('----------------------------------------------------\n');
250
260
 
251
261
  return {
@@ -262,6 +272,7 @@ export function runStatus(options = {}) {
262
272
  const engineCheck = verifyEngineBinary(binPath);
263
273
  const ledger = loadLedger();
264
274
  const scannedAgents = scanLocalAgents();
275
+ const app = detectDesktopApp();
265
276
 
266
277
  const statusObj = {
267
278
  engine: {
@@ -271,6 +282,12 @@ export function runStatus(options = {}) {
271
282
  valid: engineCheck.valid,
272
283
  deployersCount: engineCheck.deployers?.length || 0
273
284
  },
285
+ desktop_app: {
286
+ detected: Boolean(app.detected),
287
+ compatible: Boolean(app.compatible),
288
+ appPath: app.appPath || null,
289
+ version: app.version || null
290
+ },
274
291
  content: {
275
292
  dir: getContentDir(),
276
293
  manifestExists: fs.existsSync(path.join(getContentDir(), 'manifest.json'))
@@ -294,6 +311,12 @@ export function runStatus(options = {}) {
294
311
  logger(` Engine Binary: ${binPath}`);
295
312
  logger(` Engine Version: ${engineCheck.version || 'Not installed'} (${engineCheck.valid ? 'Valid' : 'Invalid'})`);
296
313
  logger(` Deployers Available: ${engineCheck.deployers?.length || 0}`);
314
+ if (app.detected) {
315
+ logger(` Desktop App (GUI): Detected at ${app.appPath || app.binPath} (v${app.version || 'installed'})`);
316
+ } else {
317
+ logger(' Desktop App (GUI): \x1b[90mNot detected\x1b[0m');
318
+ logger(' 💡 可视化看板与桌面客户端下载: https://sensecraft.seeed.cc/download');
319
+ }
297
320
  logger(` Content Directory: ${getContentDir()} (${statusObj.content.manifestExists ? 'Ready' : 'Not cached'})`);
298
321
  logger('\n Configured Agent Skills:');
299
322
 
@@ -408,3 +431,46 @@ function copyFolderSafely(srcDir, destDir, baseDestDir, priorFiles, force, logge
408
431
 
409
432
  return { filesWritten, updatedFiles, preservedFiles };
410
433
  }
434
+
435
+ /**
436
+ * Runs update on existing installed agents and synchronizes solution content and engine.
437
+ *
438
+ * @param {Object} options
439
+ * @returns {Promise<Object>}
440
+ */
441
+ export async function runUpdate(options = {}) {
442
+ const logger = options.logger || console.log;
443
+
444
+ let targetAgent = options.agent;
445
+ if (!targetAgent && !options.skillsDir) {
446
+ const scanned = scanLocalAgents();
447
+ const installed = scanned.filter(a => a.installed);
448
+ if (installed.length > 0) {
449
+ targetAgent = installed.map(a => a.id).join(',');
450
+ }
451
+ }
452
+
453
+ return runInstall({
454
+ ...options,
455
+ agent: targetAgent,
456
+ yes: options.yes !== undefined ? options.yes : true,
457
+ isUpdate: true,
458
+ logger,
459
+ });
460
+ }
461
+
462
+ /**
463
+ * Returns formatted scan of local AI agents.
464
+ *
465
+ * @returns {Array<Object>}
466
+ */
467
+ export function listAgents() {
468
+ const scanned = scanLocalAgents();
469
+ return scanned.map(a => ({
470
+ id: a.id,
471
+ name: a.name,
472
+ detected: a.detected,
473
+ installed: a.installed,
474
+ skillsDir: a.skillsDir,
475
+ }));
476
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * @file integration.js
3
+ * Integration contracts and system ingestion specifications for Solution CLI.
4
+ */
5
+
6
+ export const INTEGRATION_CONTRACTS = {
7
+ fall_detection: {
8
+ solution_id: 'fall_detection',
9
+ protocols: ['mqtt', 'http', 'webhook', 'rtsp'],
10
+ interfaces: [
11
+ {
12
+ protocol: 'mqtt',
13
+ port: 1883,
14
+ topic: '<device_name>/fall-detection/results/<stream_id>',
15
+ direction: 'publish',
16
+ description: '实时人体姿态与跌倒识别结果,一路摄像头对应一个子主题。',
17
+ payload_example: {
18
+ state: 'fallen',
19
+ fall_detected: true,
20
+ fall_event: true,
21
+ event_id: 7,
22
+ person_count: 1,
23
+ fallen_count: 1,
24
+ persons: [
25
+ {
26
+ track_id: 3,
27
+ state: 'fallen',
28
+ bbox: [0.51, 0.72, 0.34, 0.21],
29
+ confidence: 0.94
30
+ }
31
+ ],
32
+ timestamp: 1788581337000
33
+ }
34
+ },
35
+ {
36
+ protocol: 'mqtt',
37
+ port: 1883,
38
+ topic: 'homeassistant/',
39
+ direction: 'publish',
40
+ description: 'Home Assistant MQTT Discovery 自动发现协议,跌倒传感器、事件编号会自动出现。'
41
+ },
42
+ {
43
+ protocol: 'webhook',
44
+ port: 8080,
45
+ endpoint: '配置中的 ALARM_WEBHOOK_URL',
46
+ direction: 'outbound_post',
47
+ description: '跌倒告警确认后向外部系统发送 HTTP POST 通知,附带防重复通知的幂等键。',
48
+ payload_example: {
49
+ id: 'a-17',
50
+ kind: 'fall',
51
+ zone_id: 'bedroom',
52
+ stream_id: 'cam-01',
53
+ state: 'notified',
54
+ event_timestamp: 1788581337237,
55
+ operator: 'duty-nurse'
56
+ }
57
+ },
58
+ {
59
+ protocol: 'http',
60
+ port: 8080,
61
+ endpoint: '/api/alarms',
62
+ direction: 'inbound_rest',
63
+ description: '告警面板 REST API,支持查询当前告警列表、执行确认 (confirm) 与驳回 (dismiss)。'
64
+ },
65
+ {
66
+ protocol: 'rtsp',
67
+ port: 8554,
68
+ endpoint: '/live0',
69
+ direction: 'stream',
70
+ description: '设备实时处理叠加视频流,支持 NVR 录像或第三方视频播放器调阅。'
71
+ }
72
+ ],
73
+ input_requirements: [
74
+ {
75
+ type: 'rtsp',
76
+ description: '标准 RTSP 摄像头流输入(如 rtsp://admin:pass@192.168.1.100:554/live/ch0),支持海康、大华等 IP 摄像头。'
77
+ }
78
+ ],
79
+ system_ingestion_guide: [
80
+ '1. 接入自建 MQTT Broker:在方案配置中将 MQTT_BROKER 改为自建服务器地址,订阅 fall-detection/results/# 获取每帧 JSON。',
81
+ '2. 接入业务告警中心:在告警面板中配置 Webhook 地址,有人跌倒经确认后会自动向该地址 POST 结构化事件。',
82
+ '3. 接入 Home Assistant:保证同一局域网下 MQTT Broker 互通,HA 会自动发现跌倒感应实体。'
83
+ ]
84
+ },
85
+
86
+ 'edge-anpr-plate-recognition': {
87
+ solution_id: 'edge-anpr-plate-recognition',
88
+ protocols: ['mqtt', 'http', 'webhook', 'rtsp'],
89
+ interfaces: [
90
+ {
91
+ protocol: 'mqtt',
92
+ port: 1883,
93
+ topic: 'edge/anpr/results',
94
+ direction: 'publish',
95
+ description: '抓拍过车车牌识别结果上报。',
96
+ payload_example: {
97
+ plate_number: '粤B12345',
98
+ confidence: 0.98,
99
+ direction: 'inbound',
100
+ lane_id: 1,
101
+ timestamp: 1788581350000,
102
+ vehicle_color: 'white',
103
+ vehicle_type: 'sedan'
104
+ }
105
+ },
106
+ {
107
+ protocol: 'webhook',
108
+ endpoint: '配置中的 ANPR_WEBHOOK_URL',
109
+ direction: 'outbound_post',
110
+ description: '过车事件向停车计费或园区管理系统推送。'
111
+ },
112
+ {
113
+ protocol: 'rtsp',
114
+ direction: 'input',
115
+ description: 'IP 监控摄像头 RTSP 视频流接入。'
116
+ }
117
+ ],
118
+ system_ingestion_guide: [
119
+ '1. 订阅 MQTT topic "edge/anpr/results" 获取结构化车牌及进出方向。',
120
+ '2. 可配置道闸联动继电器或向道闸系统发送 HTTP 开闸指令。'
121
+ ]
122
+ },
123
+
124
+ smart_agriculture_sensing: {
125
+ solution_id: 'smart-agriculture-sensing',
126
+ protocols: ['lorawan', 'mqtt', 'modbus'],
127
+ interfaces: [
128
+ {
129
+ protocol: 'mqtt',
130
+ port: 1883,
131
+ topic: 'sensecap/data/telemetry',
132
+ direction: 'publish',
133
+ description: '农田微气象与土壤墒情周期上报。',
134
+ payload_example: {
135
+ station_id: 's700-01',
136
+ air_temperature: 24.5,
137
+ air_humidity: 65.2,
138
+ soil_moisture: 38.1,
139
+ soil_temperature: 21.0,
140
+ soil_ec: 1.2,
141
+ light_lux: 45000,
142
+ battery_pct: 95
143
+ }
144
+ },
145
+ {
146
+ protocol: 'modbus',
147
+ port: 'RS485',
148
+ direction: 'bus',
149
+ description: '标准 Modbus RTU 工业总线控制水泵与电磁阀组。'
150
+ }
151
+ ],
152
+ system_ingestion_guide: [
153
+ '1. LoRaWAN 测点通过 SenseCAP M2 网关汇聚,通过 MQTT 集中推送到企业平台。',
154
+ '2. 边缘网关就地根据阈值自动执行 Modbus 继电器启停,保证断网时本地闭环运行。'
155
+ ]
156
+ },
157
+
158
+ edge_inspection_surface: {
159
+ solution_id: 'edge_inspection_surface',
160
+ protocols: ['mqtt', 'gpio', 'http'],
161
+ interfaces: [
162
+ {
163
+ protocol: 'mqtt',
164
+ port: 1883,
165
+ topic: 'inspection/defect/events',
166
+ direction: 'publish',
167
+ description: '流水线工业质检缺陷判别事件。',
168
+ payload_example: {
169
+ part_id: 'part-20261015-081',
170
+ is_defective: true,
171
+ defect_type: 'scratch',
172
+ confidence: 0.96,
173
+ bbox: [120, 340, 45, 12],
174
+ timestamp: 1788581360000
175
+ }
176
+ },
177
+ {
178
+ protocol: 'gpio',
179
+ direction: 'digital_out',
180
+ description: '隔离数字量输出 (DO),发现次品时直接输出 24V 脉冲联动气缸执行剔除。'
181
+ }
182
+ ],
183
+ system_ingestion_guide: [
184
+ '1. 订阅缺陷事件 MQTT 用于车间看板展示不良率与缺陷类别分布。',
185
+ '2. 硬件 DO 端子直接接入产线 PLC 计数或剔除机构。'
186
+ ]
187
+ }
188
+ };
189
+
190
+ /**
191
+ * Extracts or generates system integration contract for a given solution or reference design.
192
+ *
193
+ * @param {string} solutionId
194
+ * @param {Object} [options]
195
+ * @param {string} [options.guideText] - Deployment guide text for fallback scanning
196
+ * @returns {Object} Integration contract specification
197
+ */
198
+ export function getIntegrationContract(solutionId, { guideText = '' } = {}) {
199
+ const normId = String(solutionId || '').replace(/-/g, '_').toLowerCase();
200
+ for (const [key, contract] of Object.entries(INTEGRATION_CONTRACTS)) {
201
+ const normKey = key.replace(/-/g, '_').toLowerCase();
202
+ if (normId === normKey || normId.includes(normKey) || normKey.includes(normId)) {
203
+ return contract;
204
+ }
205
+ }
206
+
207
+ // Fallback: smart heuristic scan over deployment guide text
208
+ const protocols = [];
209
+ const interfaces = [];
210
+ const g = String(guideText || '');
211
+
212
+ if (/mqtt/i.test(g)) {
213
+ protocols.push('mqtt');
214
+ interfaces.push({
215
+ protocol: 'mqtt',
216
+ port: 1883,
217
+ topic: `${solutionId}/results`,
218
+ direction: 'publish',
219
+ description: 'MQTT 业务结果与状态发布总线'
220
+ });
221
+ }
222
+ if (/webhook/i.test(g)) {
223
+ protocols.push('webhook');
224
+ interfaces.push({
225
+ protocol: 'webhook',
226
+ endpoint: '配置中的 WEBHOOK_URL',
227
+ direction: 'outbound_post',
228
+ description: '事件触发向外部系统推送 HTTP POST 回调'
229
+ });
230
+ }
231
+ if (/rtsp/i.test(g)) {
232
+ protocols.push('rtsp');
233
+ interfaces.push({
234
+ protocol: 'rtsp',
235
+ port: 8554,
236
+ direction: 'stream',
237
+ description: '视频流接入或实时画面分发'
238
+ });
239
+ }
240
+ if (/http|dashboard|api/i.test(g)) {
241
+ protocols.push('http');
242
+ interfaces.push({
243
+ protocol: 'http',
244
+ port: 8080,
245
+ endpoint: '/api',
246
+ direction: 'inbound_rest',
247
+ description: 'REST API 或管理控制面板'
248
+ });
249
+ }
250
+
251
+ if (protocols.length === 0) {
252
+ protocols.push('mqtt', 'http');
253
+ }
254
+
255
+ return {
256
+ solution_id: solutionId,
257
+ protocols,
258
+ interfaces,
259
+ input_requirements: [],
260
+ system_ingestion_guide: [
261
+ '1. 详情请查阅该方案的具体部署指南与环境变量说明。',
262
+ '2. 通过标准网络协议(MQTT/HTTP)完成与自有业务系统的对接。'
263
+ ]
264
+ };
265
+ }