@seeed-studio/solution-cli 0.3.2 → 0.4.1
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/README.md +161 -113
- package/bin/cli.js +427 -34
- package/catalog/client-config.json +2 -2
- package/catalog/knowledge-core.json +75469 -639
- package/docs/cli-guide.md +195 -227
- package/lib/agent.js +132 -23
- package/lib/catalog-dto.js +14 -1
- package/lib/doctor.js +19 -1
- package/lib/downloader.js +24 -0
- package/lib/gotchas.js +126 -0
- package/lib/installer.js +75 -9
- package/lib/integration.js +265 -0
- package/lib/knowledge.js +1140 -103
- package/package.json +4 -3
- package/skills/solution-cli/SKILL.md +120 -28
- package/skills/solution-cli/references/consult-and-select.md +58 -14
- package/skills/solution-cli/references/deploy.md +60 -248
- package/skills/solution-cli/references/device-management.md +249 -77
- package/skills/solution-cli/references/diagnose.md +15 -0
- package/skills/solution-cli/references/edge-gotchas.md +205 -0
- package/skills/solution-cli/references/generic-rk3588.md +106 -0
- package/skills/solution-cli/references/hardware-bom.md +9 -3
- package/skills/solution-cli/references/sales-feasibility.md +65 -0
- package/skills/solution-cli/references/secondary-development.md +10 -7
package/lib/agent.js
CHANGED
|
@@ -2,17 +2,72 @@ import os from 'node:os';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import readline from 'node:readline';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { loadLedger } from './ledger.js';
|
|
7
|
+
|
|
8
|
+
// Match Hermes' native data home and get_default_hermes_root() semantics.
|
|
9
|
+
function getHermesPaths() {
|
|
10
|
+
const home = os.homedir();
|
|
11
|
+
const suffix = process.env.HERMES_DATA_DIR_SUFFIX || '';
|
|
12
|
+
const nativeHome = process.platform === 'win32'
|
|
13
|
+
? path.join(process.env.LOCALAPPDATA?.trim() || path.join(home, 'AppData', 'Local'), `hermes${suffix}`)
|
|
14
|
+
: path.join(home, `.hermes${suffix}`);
|
|
15
|
+
const expand = value => value.replace(/\$(\w+)|\$\{([^}]+)\}/g,
|
|
16
|
+
(match, bare, braced) => process.env[bare || braced] ?? match)
|
|
17
|
+
.replace(/^~(?=$|[\\/])/, () => home);
|
|
18
|
+
const activeHome = path.resolve(expand(process.env.HERMES_HOME?.trim() || nativeHome));
|
|
19
|
+
const real = value => fs.existsSync(value) ? fs.realpathSync(value) : path.resolve(value);
|
|
20
|
+
const relative = path.relative(real(nativeHome), real(activeHome));
|
|
21
|
+
const inNativeHome = relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
22
|
+
const root = inNativeHome ? nativeHome
|
|
23
|
+
: path.basename(path.dirname(activeHome)) === 'profiles' ? path.dirname(path.dirname(activeHome)) : activeHome;
|
|
24
|
+
return { activeHome, root };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hermesTarget(id = 'hermes') {
|
|
28
|
+
const { activeHome, root } = getHermesPaths();
|
|
29
|
+
let targetHome = activeHome;
|
|
30
|
+
if (id !== 'hermes') {
|
|
31
|
+
const profile = id.slice('hermes:'.length);
|
|
32
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(profile)) {
|
|
33
|
+
throw new Error(`Invalid Hermes profile: "${profile}". Use 1-64 lowercase letters, numbers, '-' or '_', starting with a letter or number.`);
|
|
34
|
+
}
|
|
35
|
+
targetHome = profile === 'default' ? root : path.join(root, 'profiles', profile);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
name: id === 'hermes' ? 'Hermes Agent' : `Hermes Agent (${id.slice(7)})`,
|
|
40
|
+
homeDirs: [targetHome],
|
|
41
|
+
getSkillsDir: () => path.join(targetHome, 'skills'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scanAgent(agent) {
|
|
46
|
+
const skillsDir = agent.getSkillsDir();
|
|
47
|
+
return {
|
|
48
|
+
...agent,
|
|
49
|
+
detected: agent.homeDirs.some(dir => fs.existsSync(dir)) || fs.existsSync(skillsDir),
|
|
50
|
+
installed: fs.existsSync(path.join(skillsDir, 'solution-cli', 'SKILL.md')),
|
|
51
|
+
skillsDir,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
5
54
|
|
|
6
55
|
export function getKnownAgents() {
|
|
7
56
|
const home = os.homedir();
|
|
8
57
|
|
|
9
58
|
return [
|
|
59
|
+
{
|
|
60
|
+
id: 'doubao',
|
|
61
|
+
name: '豆包工作 (Doubao Work)',
|
|
62
|
+
homeDirs: [path.join(home, '.doubao')],
|
|
63
|
+
getSkillsDir: () => path.join(home, '.doubao', 'skills')
|
|
64
|
+
},
|
|
10
65
|
{
|
|
11
66
|
id: 'workbuddy',
|
|
12
|
-
|
|
67
|
+
aliases: ['workbody'],
|
|
68
|
+
name: 'WorkBuddy / Workbody',
|
|
13
69
|
homeDirs: [path.join(home, '.workbuddy'), path.join(home, '.workbody')],
|
|
14
70
|
getSkillsDir: () => {
|
|
15
|
-
// Check where an existing installation might already be
|
|
16
71
|
const p1 = path.join(home, '.workbuddy', 'skills');
|
|
17
72
|
const p2 = path.join(home, '.workbody', 'skills');
|
|
18
73
|
if (fs.existsSync(path.join(p1, 'solution-cli', 'SKILL.md'))) return p1;
|
|
@@ -60,48 +115,102 @@ export function getKnownAgents() {
|
|
|
60
115
|
name: 'Google Antigravity (AGY)',
|
|
61
116
|
homeDirs: [path.join(home, '.gemini', 'config'), path.join(home, '.gemini', 'antigravity-cli')],
|
|
62
117
|
getSkillsDir: () => path.join(home, '.gemini', 'config', 'skills')
|
|
63
|
-
}
|
|
118
|
+
},
|
|
119
|
+
hermesTarget()
|
|
64
120
|
];
|
|
65
121
|
}
|
|
66
122
|
|
|
67
123
|
export function scanLocalAgents() {
|
|
68
124
|
const agents = getKnownAgents();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const ledger = loadLedger();
|
|
128
|
+
for (const [targetId, inst] of Object.entries(ledger?.installations || {})) {
|
|
129
|
+
if (targetId.startsWith('hermes:') && inst && !agents.some(a => a.id === targetId)) {
|
|
130
|
+
const target = hermesTarget(targetId);
|
|
131
|
+
if (fs.existsSync(path.join(target.getSkillsDir(), 'solution-cli', 'SKILL.md'))) agents.push(target);
|
|
132
|
+
}
|
|
133
|
+
if (targetId.startsWith('custom_') && inst) {
|
|
134
|
+
const skillsDir = inst.skills_dir || (inst.target_dir ? path.dirname(inst.target_dir) : (inst.targetDir ? path.dirname(inst.targetDir) : null));
|
|
135
|
+
if (skillsDir && fs.existsSync(skillsDir) && !agents.some(a => a.id === targetId)) {
|
|
136
|
+
agents.push({
|
|
137
|
+
id: targetId,
|
|
138
|
+
name: `Custom Agent (${skillsDir})`,
|
|
139
|
+
homeDirs: [skillsDir],
|
|
140
|
+
getSkillsDir: () => skillsDir,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
|
|
147
|
+
return agents.map(scanAgent);
|
|
80
148
|
}
|
|
81
149
|
|
|
82
|
-
export
|
|
150
|
+
export function formatAgentList(scannedAgents) {
|
|
151
|
+
return scannedAgents.map(a => ({
|
|
152
|
+
id: a.id,
|
|
153
|
+
name: a.name,
|
|
154
|
+
detected: a.detected,
|
|
155
|
+
installed: a.installed,
|
|
156
|
+
skillsDir: a.skillsDir,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function promptAgentSelection(scannedAgents, cliArg, autoYes = false, skillsDir = null) {
|
|
161
|
+
// If custom skillsDir is specified, inject a custom target
|
|
162
|
+
if (skillsDir) {
|
|
163
|
+
const resolvedDir = path.resolve(skillsDir);
|
|
164
|
+
const dirHash = crypto.createHash('sha256').update(resolvedDir).digest('hex').slice(0, 8);
|
|
165
|
+
const customId = `custom_${dirHash}`;
|
|
166
|
+
const customTarget = {
|
|
167
|
+
id: customId,
|
|
168
|
+
name: `Custom Agent (${resolvedDir})`,
|
|
169
|
+
skillsDir: resolvedDir,
|
|
170
|
+
detected: true,
|
|
171
|
+
installed: fs.existsSync(path.join(resolvedDir, 'solution-cli', 'SKILL.md')),
|
|
172
|
+
homeDirs: [resolvedDir],
|
|
173
|
+
getSkillsDir: () => resolvedDir,
|
|
174
|
+
};
|
|
175
|
+
if (!cliArg) {
|
|
176
|
+
return [customTarget];
|
|
177
|
+
}
|
|
178
|
+
// Avoid duplicate if already scanned from ledger
|
|
179
|
+
const existingIdx = scannedAgents.findIndex(a => a.id === customId);
|
|
180
|
+
if (existingIdx >= 0) {
|
|
181
|
+
scannedAgents[existingIdx] = customTarget;
|
|
182
|
+
} else {
|
|
183
|
+
scannedAgents.push(customTarget);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
83
187
|
// If explicitly specified in CLI flags
|
|
84
188
|
if (cliArg) {
|
|
85
189
|
if (cliArg.toLowerCase() === 'all') {
|
|
86
190
|
return scannedAgents;
|
|
87
191
|
}
|
|
88
|
-
const requested = cliArg.split(',').map(s => s.trim()
|
|
89
|
-
|
|
192
|
+
const requested = cliArg.split(',').map(s => s.trim());
|
|
193
|
+
// Validate profile IDs before normalizing other hosts or accepting a partial match.
|
|
194
|
+
for (const id of requested) {
|
|
195
|
+
if (id.toLowerCase().startsWith('hermes:')) {
|
|
196
|
+
const target = scanAgent(hermesTarget(`hermes:${id.slice(7)}`));
|
|
197
|
+
if (!scannedAgents.some(a => a.id === target.id)) scannedAgents.push(target);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const normalized = requested.map(id => id.toLowerCase());
|
|
201
|
+
const matched = scannedAgents.filter(a => normalized.includes(a.id) || a.aliases?.some(alias => normalized.includes(alias)));
|
|
90
202
|
if (matched.length === 0) {
|
|
91
203
|
throw new Error(`No matching agents for: "${cliArg}". Available: ${scannedAgents.map(a => a.id).join(', ')}`);
|
|
92
204
|
}
|
|
93
205
|
return matched;
|
|
94
206
|
}
|
|
95
207
|
|
|
96
|
-
// Non-interactive check (CI / piped / daemon)
|
|
208
|
+
// Non-interactive check (CI / piped / daemon / AI Agent subprocess)
|
|
97
209
|
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
98
210
|
|
|
99
211
|
if (!isInteractive) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return detectedOnly.length > 0 ? detectedOnly : scannedAgents.slice(0, 1);
|
|
103
|
-
}
|
|
104
|
-
throw new Error('Non-interactive environment detected. Please specify --agent <workbuddy|claude|codex|codebuddy|all> or pass -y.');
|
|
212
|
+
const detectedOnly = scannedAgents.filter(a => a.detected);
|
|
213
|
+
return detectedOnly.length > 0 ? detectedOnly : scannedAgents;
|
|
105
214
|
}
|
|
106
215
|
|
|
107
216
|
if (autoYes) {
|
package/lib/catalog-dto.js
CHANGED
|
@@ -13,6 +13,10 @@ export const SCHEMA_URL = 'https://spec.seeedstudio.com/schemas/v2/catalog-dto.j
|
|
|
13
13
|
* @param {'FRESH' | 'STALE' | 'UNKNOWN'} [options.freshness='FRESH']
|
|
14
14
|
* @param {string} [options.datasetVersion='2026-10-13.1']
|
|
15
15
|
* @param {Array} options.records
|
|
16
|
+
* @param {Object} [options.pagination]
|
|
17
|
+
* @param {string} [options.scope]
|
|
18
|
+
* @param {string} [options.locale]
|
|
19
|
+
* @param {Object} [options.provenance]
|
|
16
20
|
* @param {Object} [options.fieldCompleteness]
|
|
17
21
|
* @param {Array<string>} [options.warnings=[]]
|
|
18
22
|
* @returns {Object} Validated Catalog DTO
|
|
@@ -22,10 +26,14 @@ export function createCatalogDto({
|
|
|
22
26
|
freshness = 'FRESH',
|
|
23
27
|
datasetVersion = '2026-10-13.1',
|
|
24
28
|
records = [],
|
|
29
|
+
pagination = null,
|
|
30
|
+
scope = null,
|
|
31
|
+
locale = null,
|
|
32
|
+
provenance = null,
|
|
25
33
|
fieldCompleteness = { spec: true, compatibility: true, offer: source === 'typesense' },
|
|
26
34
|
warnings = [],
|
|
27
35
|
}) {
|
|
28
|
-
|
|
36
|
+
const dto = {
|
|
29
37
|
$schema: SCHEMA_URL,
|
|
30
38
|
schema_version: SCHEMA_VERSION,
|
|
31
39
|
dataset_version: datasetVersion,
|
|
@@ -37,6 +45,11 @@ export function createCatalogDto({
|
|
|
37
45
|
records: Array.isArray(records) ? records : [],
|
|
38
46
|
warnings: Array.isArray(warnings) ? warnings : [],
|
|
39
47
|
};
|
|
48
|
+
if (pagination) dto.pagination = pagination;
|
|
49
|
+
if (scope) dto.scope = scope;
|
|
50
|
+
if (locale) dto.locale = locale;
|
|
51
|
+
if (provenance) dto.provenance = provenance;
|
|
52
|
+
return dto;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
/**
|
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(
|
|
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.
|
|
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
|
-
|
|
151
|
-
|
|
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👉
|
|
247
|
-
logger('
|
|
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
|
+
}
|