@seeed-studio/solution-cli 0.3.0 → 0.3.2

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/knowledge.js CHANGED
@@ -144,8 +144,12 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
144
144
  const core = loadKnowledgeCore();
145
145
  const onlinePromises = [];
146
146
 
147
+ const locKey = locale === 'zh' ? 'zh_hans' : locale;
148
+ const fetchLimit = Math.max(limit * 5, 30);
149
+
147
150
  if (keySolution) {
148
- const solUrl = `${host}/collections/Solution/documents/search?q=${encodeURIComponent(query)}&query_by=title_zh_hans,title_en,excerpt_zh_hans,excerpt_en,display_tags_zh_hans&per_page=${limit}`;
151
+ const solFilter = 'published:!=false';
152
+ const solUrl = `${host}/collections/Solution/documents/search?q=${encodeURIComponent(query)}&query_by=title_zh_hans,title_en,excerpt_zh_hans,excerpt_en,display_tags_zh_hans&filter_by=${encodeURIComponent(solFilter)}&per_page=${fetchLimit}`;
149
153
  onlinePromises.push(
150
154
  fetchJson(solUrl, { 'X-TYPESENSE-API-KEY': keySolution }, 2500)
151
155
  .then((data) => ({ type: 'scenario', hits: data?.hits || [] }))
@@ -154,7 +158,8 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
154
158
  }
155
159
 
156
160
  if (keyRefDesign) {
157
- const rdUrl = `${host}/collections/reference_design/documents/search?q=${encodeURIComponent(query)}&query_by=name_zh_hans,name_en,summary_zh_hans,summary_en,tags&per_page=${limit}`;
161
+ const rdFilter = 'published:!=false';
162
+ const rdUrl = `${host}/collections/reference_design/documents/search?q=${encodeURIComponent(query)}&query_by=name_zh_hans,name_en,summary_zh_hans,summary_en,tags&filter_by=${encodeURIComponent(rdFilter)}&per_page=${fetchLimit}`;
158
163
  onlinePromises.push(
159
164
  fetchJson(rdUrl, { 'X-TYPESENSE-API-KEY': keyRefDesign }, 2500)
160
165
  .then((data) => ({ type: 'reference_design', hits: data?.hits || [] }))
@@ -163,13 +168,19 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
163
168
  }
164
169
 
165
170
  const results = await Promise.all(onlinePromises);
166
- const onlineRecords = [];
171
+ const onlineScenarios = [];
172
+ const onlineRefDesigns = [];
167
173
 
168
174
  for (const res of results) {
169
175
  if (res.type === 'scenario') {
170
176
  for (const hit of res.hits) {
171
177
  const doc = hit.document || {};
178
+ // Filter unreleased or staging scenarios from production recommendations
179
+ if (doc.published === false || doc.visibility === 'staging' || doc.visibility === 'draft') continue;
180
+ if (doc[`visibility_${locKey}`] === 'staging' || doc[`visibility_${locKey}`] === 'draft') continue;
181
+
172
182
  const docId = doc.id || doc.slug;
183
+ const docType = doc.doc_type || 'scenario';
173
184
  const title = locale === 'en'
174
185
  ? (doc.title_en || doc.title_zh_hans || docId)
175
186
  : (doc.title_zh_hans || doc.title_en || docId);
@@ -178,22 +189,23 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
178
189
  : (doc.excerpt_zh_hans || doc.seo_description_zh_hans || doc.excerpt_en || '');
179
190
  const tags = (locale === 'en' ? doc.display_tags_en : doc.display_tags_zh_hans) || doc.display_tags || [];
180
191
  const pb = core.diagnostic_playbooks?.[docId] || doc.diagnostic_playbook || null;
192
+ const rawLanding = doc.external_url || doc.landing_url || `https://www.seeedstudio.com/solutions/${docId}`;
181
193
 
182
- onlineRecords.push({
183
- type: 'scenario',
194
+ onlineScenarios.push({
195
+ type: docType,
184
196
  id: docId,
185
197
  title,
186
198
  summary,
187
199
  tags,
188
200
  family_ids: doc.family_ids || [],
189
201
  reference_designs: doc.reference_designs || [],
190
- landing_url: appendUtm(`https://www.seeedstudio.com/solutions/${docId}`, {
202
+ landing_url: appendUtm(rawLanding, {
191
203
  source: 'sensecraft_agent',
192
204
  medium: 'solution_consult',
193
205
  campaign: docId,
194
- content: 'scenario_landing',
206
+ content: `${docType}_landing`,
195
207
  }),
196
- repo_url: '',
208
+ repo_url: doc.repo_url || '',
197
209
  diagnostic_playbook: pb,
198
210
  source: 'typesense',
199
211
  });
@@ -201,6 +213,10 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
201
213
  } else if (res.type === 'reference_design') {
202
214
  for (const hit of res.hits) {
203
215
  const doc = hit.document || {};
216
+ // Filter unreleased or staging reference designs from production recommendations
217
+ if (doc.published === false || doc.visibility === 'staging' || doc.visibility === 'draft') continue;
218
+ const locKey = locale === 'zh' ? 'zh_hans' : locale;
219
+ if (doc[`visibility_${locKey}`] === 'staging' || doc[`visibility_${locKey}`] === 'draft') continue;
204
220
  const docId = doc.id;
205
221
  const title = locale === 'en'
206
222
  ? (doc.name_en || doc.name_zh_hans || docId)
@@ -209,7 +225,7 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
209
225
  ? (doc.summary_en || doc.tagline_en || doc.summary_zh_hans || '')
210
226
  : (doc.summary_zh_hans || doc.tagline_zh_hans || doc.summary_en || '');
211
227
 
212
- let pb = doc.diagnostic_playbook || null;
228
+ let pb = doc.diagnostic_playbook || core.diagnostic_playbooks?.[docId] || null;
213
229
  if (!pb) {
214
230
  for (const sc of core.scenarios || []) {
215
231
  if ((sc.reference_designs || []).includes(docId)) {
@@ -219,12 +235,24 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
219
235
  }
220
236
  }
221
237
 
222
- onlineRecords.push({
238
+ const rawPresets = doc.presets || [];
239
+ const presets = rawPresets.map((p) => {
240
+ if (typeof p === 'string') return p;
241
+ if (p && typeof p === 'object') {
242
+ return (locale === 'en' ? (p.name_en || p.name || p.id) : (p.name_zh_hans || p.name || p.id)) || p.id || '';
243
+ }
244
+ return String(p || '');
245
+ }).filter(Boolean);
246
+
247
+ const bundledRd = (core.reference_designs || []).find((r) => r.id === docId);
248
+ const repoUrl = doc.repo_url || bundledRd?.repo_url || '';
249
+
250
+ onlineRefDesigns.push({
223
251
  type: 'reference_design',
224
252
  id: docId,
225
253
  title,
226
254
  summary,
227
- presets: doc.presets || [],
255
+ presets,
228
256
  landing_url: appendUtm(`https://www.seeedstudio.com/solutions/reference-designs/${docId}`, {
229
257
  source: 'sensecraft_agent',
230
258
  medium: 'solution_consult',
@@ -236,7 +264,7 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
236
264
  medium: 'tech_wiki',
237
265
  campaign: docId,
238
266
  }) : '',
239
- repo_url: doc.repo_url || `https://github.com/suharvest/sensecraft-solutions/tree/main/solutions/${docId}`,
267
+ repo_url: repoUrl,
240
268
  diagnostic_playbook: pb,
241
269
  source: 'typesense',
242
270
  });
@@ -244,11 +272,90 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
244
272
  }
245
273
  }
246
274
 
247
- if (onlineRecords.length > 0) {
275
+ // If one collection returned results but the other was unavailable/failed/empty, supplement from bundled catalog
276
+ if ((onlineScenarios.length > 0 && onlineRefDesigns.length === 0) || (onlineRefDesigns.length > 0 && onlineScenarios.length === 0)) {
277
+ const tokens = qLower.split(/\s+/).filter(Boolean);
278
+ if (onlineScenarios.length === 0) {
279
+ for (const s of core.scenarios || []) {
280
+ if (s.published === false || s.visibility === 'staging' || s.visibility === 'draft') continue;
281
+ const locKey = locale === 'zh' ? 'zh_hans' : locale;
282
+ if (s[`visibility_${locKey}`] === 'staging' || s[`visibility_${locKey}`] === 'draft') continue;
283
+
284
+ const haystack = [s.id, s.title?.zh, s.title?.en, s.subtitle?.zh, s.subtitle?.en, s.summary, ...(s.tags || [])].join(' ').toLowerCase();
285
+ const score = tokens.reduce((acc, t) => (haystack.includes(t) ? acc + 1 : acc), 0);
286
+ if (tokens.length === 0 || score > 0) {
287
+ onlineScenarios.push({
288
+ type: 'scenario',
289
+ id: s.id,
290
+ title: s.title?.[locale] || s.title?.zh || s.title?.en || s.id,
291
+ summary: s.subtitle?.[locale] || s.subtitle?.zh || s.summary || '',
292
+ tags: s.tags || [],
293
+ family_ids: s.family_ids || [],
294
+ reference_designs: s.reference_designs || [],
295
+ landing_url: appendUtm(s.landing_url || `https://www.seeedstudio.com/solutions/${s.id}`, {
296
+ source: 'sensecraft_agent',
297
+ medium: 'solution_consult',
298
+ campaign: s.id,
299
+ content: 'scenario_landing',
300
+ }),
301
+ repo_url: s.repo_url || '',
302
+ diagnostic_playbook: s.diagnostic_playbook || null,
303
+ source: 'bundled_manifest',
304
+ });
305
+ }
306
+ }
307
+ }
308
+ if (onlineRefDesigns.length === 0) {
309
+ for (const rd of core.reference_designs || []) {
310
+ if (rd.published === false || rd.visibility === 'staging' || rd.visibility === 'draft') continue;
311
+ const locKey = locale === 'zh' ? 'zh_hans' : locale;
312
+ if (rd[`visibility_${locKey}`] === 'staging' || rd[`visibility_${locKey}`] === 'draft') continue;
313
+
314
+ const haystack = [rd.id, rd.title?.zh, rd.title?.en, rd.summary?.zh, rd.summary?.en, ...(rd.presets || [])].join(' ').toLowerCase();
315
+ const score = tokens.reduce((acc, t) => (haystack.includes(t) ? acc + 1 : acc), 0);
316
+ if (tokens.length === 0 || score > 0) {
317
+ onlineRefDesigns.push({
318
+ type: 'reference_design',
319
+ id: rd.id,
320
+ title: rd.title?.[locale] || rd.title?.zh || rd.title?.en || rd.id,
321
+ summary: rd.summary?.[locale] || rd.summary?.zh || rd.summary?.en || '',
322
+ presets: rd.presets || [],
323
+ landing_url: appendUtm(rd.landing_url || `https://www.seeedstudio.com/solutions/reference-designs/${rd.id}`, {
324
+ source: 'sensecraft_agent',
325
+ medium: 'solution_consult',
326
+ campaign: rd.id,
327
+ content: 'solution_landing',
328
+ }),
329
+ wiki_url: rd.wiki_url ? appendUtm(rd.wiki_url, {
330
+ source: 'sensecraft_agent',
331
+ medium: 'tech_wiki',
332
+ campaign: rd.id,
333
+ }) : '',
334
+ repo_url: rd.repo_url || '',
335
+ diagnostic_playbook: rd.diagnostic_playbook || null,
336
+ source: 'bundled_manifest',
337
+ });
338
+ }
339
+ }
340
+ }
341
+ }
342
+
343
+ const mergedRecords = [];
344
+ const maxOnline = Math.max(onlineScenarios.length, onlineRefDesigns.length);
345
+ for (let i = 0; i < maxOnline && mergedRecords.length < limit; i++) {
346
+ if (i < onlineScenarios.length && mergedRecords.length < limit) {
347
+ mergedRecords.push(onlineScenarios[i]);
348
+ }
349
+ if (i < onlineRefDesigns.length && mergedRecords.length < limit) {
350
+ mergedRecords.push(onlineRefDesigns[i]);
351
+ }
352
+ }
353
+
354
+ if (mergedRecords.length > 0) {
248
355
  return createCatalogDto({
249
- source: 'typesense',
356
+ source: (onlineScenarios.some(r => r.source === 'typesense') || onlineRefDesigns.some(r => r.source === 'typesense')) ? 'typesense' : 'bundled_manifest',
250
357
  freshness: 'FRESH',
251
- records: onlineRecords.slice(0, limit),
358
+ records: mergedRecords,
252
359
  });
253
360
  }
254
361
  } catch {
@@ -264,6 +371,10 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
264
371
  const tokens = qLower.split(/\s+/).filter(Boolean);
265
372
 
266
373
  for (const s of core.scenarios || []) {
374
+ if (s.published === false || s.visibility === 'staging' || s.visibility === 'draft') continue;
375
+ const locKey = locale === 'zh' ? 'zh_hans' : locale;
376
+ if (s[`visibility_${locKey}`] === 'staging' || s[`visibility_${locKey}`] === 'draft') continue;
377
+
267
378
  const haystack = [
268
379
  s.id,
269
380
  s.title?.zh,
@@ -298,6 +409,10 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
298
409
  }
299
410
 
300
411
  for (const rd of core.reference_designs || []) {
412
+ if (rd.published === false || rd.visibility === 'staging' || rd.visibility === 'draft') continue;
413
+ const locKey = locale === 'zh' ? 'zh_hans' : locale;
414
+ if (rd[`visibility_${locKey}`] === 'staging' || rd[`visibility_${locKey}`] === 'draft') continue;
415
+
301
416
  const haystack = [
302
417
  rd.id,
303
418
  rd.title?.zh,
@@ -326,7 +441,7 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
326
441
  medium: 'tech_wiki',
327
442
  campaign: rd.id,
328
443
  }) : '',
329
- repo_url: rd.repo_url || `https://github.com/suharvest/sensecraft-solutions/tree/main/solutions/${rd.id}`,
444
+ repo_url: rd.repo_url || '',
330
445
  diagnostic_playbook: rd.diagnostic_playbook || null,
331
446
  score,
332
447
  });
@@ -337,7 +452,16 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
337
452
  matchedScenarios.sort((a, b) => b.score - a.score);
338
453
  matchedRefDesigns.sort((a, b) => b.score - a.score);
339
454
 
340
- const combined = [...matchedScenarios, ...matchedRefDesigns].slice(0, limit);
455
+ const combined = [];
456
+ const maxOffline = Math.max(matchedScenarios.length, matchedRefDesigns.length);
457
+ for (let i = 0; i < maxOffline && combined.length < limit; i++) {
458
+ if (i < matchedScenarios.length && combined.length < limit) {
459
+ combined.push(matchedScenarios[i]);
460
+ }
461
+ if (i < matchedRefDesigns.length && combined.length < limit) {
462
+ combined.push(matchedRefDesigns[i]);
463
+ }
464
+ }
341
465
 
342
466
  return createCatalogDto({
343
467
  source: 'bundled_manifest',
package/lib/ledger.js CHANGED
@@ -246,7 +246,7 @@ export function removeInstallation(agentId, options = {}) {
246
246
  }
247
247
 
248
248
  // Remove marker file
249
- const marker = path.join(targetDir, '.installed-by-sensecraft');
249
+ const marker = path.join(targetDir, '.installed-by-solution-cli');
250
250
  if (fs.existsSync(marker) && isSafePathInside(marker, targetDir)) {
251
251
  actions.push({ file: marker, action: 'delete_marker' });
252
252
  if (!dryRun) {
package/package.json CHANGED
@@ -1,15 +1,12 @@
1
1
  {
2
2
  "name": "@seeed-studio/solution-cli",
3
- "version": "0.3.0",
4
- "description": "Zero-friction installer and CLI runner for SenseCraft Engine and AI Agent Skills (Work Buddy, Claude Code, Codex, CodeBuddy)",
3
+ "version": "0.3.2",
4
+ "description": "Installer for the Rust deployment engine and Solution CLI Agent skills (Work Buddy, Claude Code, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "./lib/installer.js",
7
7
  "bin": {
8
8
  "solution-cli": "bin/cli.js",
9
- "sensecraft-solution-cli": "bin/cli.js",
10
- "sensecraft": "bin/cli.js",
11
- "sensecraft-engine": "bin/runner.js",
12
- "sensecraft-skill": "bin/cli.js"
9
+ "sensecraft-engine": "bin/runner.js"
13
10
  },
14
11
  "publishConfig": {
15
12
  "access": "public"
@@ -1,11 +1,11 @@
1
1
  ---
2
- name: sensecraft
3
- description: Consult, select, deploy, manage, and troubleshoot SenseCraft / Seeed Studio edge IoT solutions (智慧农业, 摔倒检测, 边缘安全, 智慧仓库, 语音助手, people counting, face recognition 等) and hardware products (reComputer Jetson, reCamera, SenseCAP, Grove Vision AI, ESP32) using the `sensecraft-engine` and `solution-cli` toolset. Use whenever the user asks for edge IoT solutions, hardware recommendations, accessory compatibility, deployment, or troubleshooting.
2
+ name: solution-cli
3
+ description: 用于客户询盘、客户需求、方案选型、我们能提供什么、怎么回复客户等边缘/IoT 方案咨询,涵盖智慧农业、跌倒检测、楼宇温控、恒温器、传感器及相关硬件。Use for customer inquiries, requirements analysis, solution selection, proposal guidance, edge IoT deployments, hardware compatibility, deployment, and troubleshooting. First search the SenseCraft solution catalog with `solution-cli`; do not trigger for general business writing, generic proposal drafting, or unrelated coding.
4
4
  ---
5
5
 
6
- # SenseCraft Solution
6
+ # Solution CLI
7
7
 
8
- This skill drives the `sensecraft-engine` execution engine and `solution-cli` toolset. It operates across the complete lifecycle: **Solution Consultation ➔ Hardware & Accessory BOM Selection ➔ Local Deployment ➔ Self-Healing Diagnostics**.
8
+ This skill drives the `sensecraft-engine` execution engine and `solution-cli` toolset. It operates across the complete lifecycle: **Solution Consultation ➔ Hardware & Accessory BOM Selection ➔ Local Deployment ➔ Self-Healing Diagnostics**. For Engine-only operations, use the injected absolute Engine path. If the wrapper is not on `PATH`, invoke the CLI as `npx -y --package=@seeed-studio/solution-cli@latest solution-cli <args>`. Keep catalog queries such as `hub` and `product` on `solution-cli`; do not forward them to the Engine.
9
9
 
10
10
  ## Workflow Routing & Progressive Disclosure
11
11
 
@@ -16,7 +16,7 @@ Do NOT read all reference files at once. Determine the user's workflow state and
16
16
  | **DISCOVERY / CONSULT**<br>“我想做防跌倒监控”、“有智慧农场方案吗” | `references/consult-and-select.md` | `solution-cli hub search "<keyword>" --json` | **DO NOT run deploy**; user is exploring scenarios and has not confirmed hardware. |
17
17
  | **PROCUREMENT / VERIFY_BOM**<br>“买什么硬件?”、“J4012 需要配什么配件?” | `references/hardware-bom.md` | `solution-cli product accessories <id> --json` | **DO NOT fabricate** third-party SKUs or prices. Always distinguish REQUIRED vs OPTIONAL accessories. |
18
18
  | **ONBOARDING / PRECHECK**<br>“设备到了,插上了网线/USB”、“环境自检” | `references/diagnose.md` | `solution-cli doctor` / `sensecraft-engine device scan` | Freeze hardware requirements into DeploymentLockfile before proceeding to apply. |
19
- | **DEPLOYING / APPLY**<br>“开始安装”、“部署 fall_detection” | `references/deploy.md` | `sensecraft-engine deploy --solution <id>` | **DEPLOYMENT CRITICAL SECTION**: Zero external commercial queries allowed during apply. |
19
+ | **DEPLOYING / APPLY**<br>“开始安装”、“部署 fall_detection” | `references/deploy.md` | `sensecraft-engine deploy <id>` | **DEPLOYMENT CRITICAL SECTION**: Zero external commercial queries allowed during apply. |
20
20
  | **MANAGE / RECONFIG**<br>“改一下 MQTT 地址”、“查看已安装应用”、“重启服务”、“修改配置” | `references/device-management.md` | `curl BASE/api/docker-devices/local/config/<id>` | **Check before write**: Only alter `reconfigurable: true` fields; check current value before modifying. |
21
21
  | **CUSTOMIZE / SECONDARY_DEV**<br>“我想改源码”、“换成自研模型”、“改前端看板”、“GitHub 仓库” | `references/secondary-development.md` | `git clone <repo_url>` / `sensecraft-engine --solutions-dir` | **Never edit cached content**: Clone to user workspace, pair-program, test locally with `--solutions-dir`. |
22
22
  | **TROUBLESHOOTING**<br>“部署报错”、“黑屏”、“无视频流” | `references/error-catalog.md` | `sensecraft-engine logs` / `solution-cli product verify-compatibility` | Max 3 fix-and-retry rounds. Follow local auto-patching rule. |
@@ -78,7 +78,7 @@ daemon is a host prerequisite, not evidence that a deployment has passed.
78
78
  you checked, what you changed). Transient registry or network errors follow
79
79
  the backoff rule in `references/deploy.md` §6 and do not use up a round.
80
80
  4. **Preserving official cache & Local auto-patching**: Never edit files directly under the official content cache directory (`~/.sensecraft/content/`). When fixing a solution-level bug (timeout too short, wrong port/env var, minor syntax/transfer glitch, broken health check):
81
- - In CLI / Engine mode: copy the solution from `~/.sensecraft/content/solutions/<id>` into `./.sensecraft-work/solutions/<id>`, apply the minimal fix in that copy. **Crucial engine switch step**: Because an existing engine daemon instance retains its initial solution directory in memory, you MUST stop the running instance first: `sensecraft-engine daemon stop`. Then re-run deployment with `--solutions-dir ./.sensecraft-work/solutions` so the fresh engine instance picks up the patched files. Show the user the patch diff.
81
+ - In CLI / Engine mode: copy the solution from `~/.sensecraft/content/solutions/<id>` into `./.sensecraft-work/solutions/<id>`, apply the minimal fix in that copy. Then re-run deployment with `sensecraft-engine deploy <id> --solutions-dir ./.sensecraft-work/solutions` so the engine picks up the patched files. Show the user the patch diff.
82
82
  - If running with the Desktop App: use the app's edit copy mechanism (`references/app-only.md` §4).
83
83
  5. **Reading output:** read the CLI's JSON output directly; it is small. If a
84
84
  filter is needed, use `jq` only when it is already installed. Do not start
@@ -103,7 +103,7 @@ daemon is a host prerequisite, not evidence that a deployment has passed.
103
103
  asset reuse, power/cabling, inspection speed/materials) to help the user converge
104
104
  on the optimal hardware architecture and budget.
105
105
 
106
- ## Step 0 — Install the engine and the solutions (every task)
106
+ ## Step 0 — Install the engine and the solutions (when needed)
107
107
 
108
108
  1. Check whether it is installed: `sensecraft-engine --version`. If the command
109
109
  is not found, also try `~/.sensecraft/bin/sensecraft-engine --version`
@@ -121,14 +121,41 @@ daemon is a host prerequisite, not evidence that a deployment has passed.
121
121
  command and stop; do not install packages or start another interpreter.
122
122
  `minisign` is required to verify the signed release manifest; Unix `jq` is
123
123
  required by the shell installer to read it.
124
- 3. Not installed → install it together with the solutions (ask the user first,
125
- it downloads a program):
124
+ 3. If it is not installed, install it together with the solutions (ask the user
125
+ first, it downloads a program). If the user explicitly asks to upgrade or
126
+ reinstall, use the same solution-cli command; do not reinstall on every
127
+ deployment:
126
128
 
127
- **Recommended (Zero-friction via solution-cli, auto-detects existing app & agents)**:
129
+ **Recommended (Zero-friction via solution-cli, target the current Agent explicitly)**:
128
130
  ```bash
129
- npx @seeed-studio/solution-cli install -y
131
+ npx -y @seeed-studio/solution-cli@latest install --agent <current-agent-id> -y
130
132
  ```
131
133
 
134
+ Fill `<current-agent-id>` with the actual host ID: `workbuddy`, `claude`,
135
+ `codex`, `codebuddy`, or `antigravity`. The WorkBuddy example is:
136
+ ```bash
137
+ npx -y @seeed-studio/solution-cli@latest install --agent workbuddy -y
138
+ ```
139
+ An AI agent must use its current host ID; Codex must not hardcode WorkBuddy.
140
+ Use `npx -y @seeed-studio/solution-cli@latest install` for interactive
141
+ selection. When one or more Agents are detected, pressing Enter selects all
142
+ detected Agents; entering the matching number selects one host. In
143
+ non-interactive mode, `install -y` without `--agent` also selects all detected
144
+ Agents when any are detected. If none are detected, these defaults still use
145
+ the CLI's default target set; pass `--agent <agent-id>` to control the scope
146
+ explicitly.
147
+
148
+ The command checks the signed stable Engine on fresh and repeat runs, reuses
149
+ an equal or newer local version, and upgrades an older one. It preserves
150
+ modified/unregistered skills and protected symlinks. Add `--agent ...` for an
151
+ explicit agent selection or `--engine-path <path>` for a valid custom pin.
152
+ `--force` can re-download equal/older versions and overwrite existing skill
153
+ files, including user modifications; use it only when that overwrite is
154
+ intended. Protected external symlinks are not followed, and it never
155
+ downgrades a newer discovered Engine. Automatic reuse/upgrade rules start with
156
+ publicly published solution-cli 0.3.1. Latest/signature failure
157
+ stops before writes; offline use requires a valid explicit local Engine path.
158
+
132
159
  **Fallback (Standalone shell script when Node.js is not present)**:
133
160
  ```bash
134
161
  curl --compressed -fsSL https://sensecraft-statics.seeed.cc/solution-app/engine/install.sh | sh -s -- --with-content
@@ -162,9 +189,8 @@ daemon is a host prerequisite, not evidence that a deployment has passed.
162
189
  list` or `deploy-info` exits with code 3 when no usable resource/solution
163
190
  directory is available and tells you to run `content pull`. If a usable
164
191
  directory exists but the requested solution ID is unknown, the query exits
165
- with code 2. After `content pull` / `update`, if an engine instance is
166
- already running it keeps the solutions it loaded at start: run
167
- `sensecraft-engine daemon stop` when no deployment is running.
192
+ with code 2. After `content pull` / `update`, newly downloaded or updated
193
+ solutions are available to subsequent deployment and query commands.
168
194
  5. Device pre-checks on this computer: `sensecraft-engine doctor --json`. Each
169
195
  check has `status` (`ok` / `warn` / `fail`) and a `fix` command. Show the
170
196
  user any `fail` and its `fix`; running a `fix` changes this computer and
@@ -18,6 +18,8 @@
18
18
  | **建筑能耗 / 暖通** | 楼宇能耗、电表、水表、空调节能、HVAC, energy retrofit | `building-energy-retrofit` | `building_energy_management` |
19
19
  | **室内外定位 / 资产** | 蓝牙信标、资产定位、人员卡片、防丢、GNSS, tracker, beacon, T1000 | `indoor-outdoor-positioning` | `indoor_positioning_ble_lorawan` |
20
20
 
21
+ 客户询盘、客户需求分析、方案选型、如何回复客户或“我们能提供什么”也属于咨询路由;涉及楼宇温控、恒温器、温度/湿度/空气质量传感器等需求时,先用客户原话和关键技术词搜索方案库。若搜不到匹配结果,明确说明官方方案库缺少现成能力,再给出需定制验证的边界;不要把通用 IoT 能力编造成已有的 thermostat 硬件或现成方案。
22
+
21
23
  ---
22
24
 
23
25
  ## 2. 工具调用标准动作
@@ -27,6 +29,8 @@
27
29
  ```bash
28
30
  solution-cli hub search "<用户关键词>" --limit 5 --json
29
31
  ```
32
+ - **咨询顺序**:先用用户原话和同义技术词查询,再根据返回的场景与 `diagnostic_playbook` 拆解需求;向用户确认点位、协议、供电、通信、控制闭环和预算等关键参数后,给出可提供的方案、硬件方向和缺口说明。
33
+ - **交付边界**:咨询阶段只做检索、需求澄清和方案建议,不自动执行 `deploy`;用户确认硬件和部署目标后,再转入硬件校验或部署流程。
30
34
  - **输出结果说明**:
31
35
  - `Scenario`:业务场景、痛点、成效、官方场景落地页(`landing_url`);
32
36
  - `Reference Design`:技术路线、预设模式 presets、开发 wiki(`wiki_url`)、官方方案落地页(`landing_url`)、源码仓库(`repo_url`);
@@ -22,50 +22,57 @@
22
22
  当设备已经部署运行,用户希望修改 L1 参数(如更改 MQTT 目标服务器)时:
23
23
 
24
24
  ### 步骤一:获取运行时信息与当前部署列表
25
- 读取运行环境指针(从 `~/.sensecraft/runtime.json` 获取 `base_url`,默认为 `http://127.0.0.1:3260`):
25
+ 读取运行环境指针并校验服务健康(桌面端每次启动分配随机端口并写入 `~/.sensecraft/runtime.json`;源码开发环境固定为 `http://127.0.0.1:3260`):
26
26
  ```bash
27
+ # 解析运行时 BASE_URL 并校验连通性
28
+ BASE_URL=$(jq -r '.base_url // "http://127.0.0.1:3260"' ~/.sensecraft/runtime.json 2>/dev/null || echo "http://127.0.0.1:3260")
29
+ curl -sf "$BASE_URL/api/health" > /dev/null || { echo "SenseCraft 服务未在 $BASE_URL 就绪"; exit 1; }
30
+
27
31
  # 查询当前设备上活跃运行的方案实例
28
- curl -s "http://127.0.0.1:3260/api/device-management/active"
32
+ curl -s "$BASE_URL/api/device-management/active"
29
33
  ```
30
34
  - 输出包含 `solution_id`、`device_id`、`status`(running/stopped)、`deployed_at` 及运行 URL。
31
35
 
32
36
  ### 步骤二:读取可配参数及当前值(Get Config)
33
37
  ```bash
34
38
  # 本地 Docker 部署实例
35
- curl -s "http://127.0.0.1:3260/api/docker-devices/local/config/<solution_id>"
39
+ curl -s "$BASE_URL/api/docker-devices/local/config/<solution_id>"
36
40
 
37
41
  # 远程设备实例(通过 SSH 提取运行态值)
38
- curl -s -X POST "http://127.0.0.1:3260/api/docker-devices/config/<solution_id>" \
42
+ curl -s -X POST "$BASE_URL/api/docker-devices/config/<solution_id>" \
39
43
  -H "Content-Type: application/json" \
40
44
  -d '{"connection": {"host": "<ip>", "username": "<user>", "password": "<pwd>"}}'
41
45
  ```
42
46
  - 检查返回的 `fields` 列表:
43
- - 确认用户想要修改的参数是否存在(`field.id`);
47
+ - 确认用户想要修改的参数是否存在(`field.id`,如 `mqtt_broker`、`mqtt_port`);
44
48
  - 查看当前已生效的数值(`field.current_value`);
45
49
  - 核对其校验规则(`field.validation`)。
46
50
 
47
51
  ### 步骤三:验证并提交新参数(Update Config)
52
+ > [!IMPORTANT]
53
+ > `values` 字典中的 key 必须使用步骤二返回的 `field.id`(例如 `mqtt_broker`、`mqtt_port`),严禁使用环境变量大写名(如 `MQTT_BROKER_HOST`),否则会被后端配置解析器静默忽略。
54
+
48
55
  向用户确认修改后,发起热更新请求:
49
56
  ```bash
50
- # 本地 Docker 部署实例更新
51
- curl -s -X PUT "http://127.0.0.1:3260/api/docker-devices/local/config/<solution_id>" \
57
+ # 本地 Docker 部署实例更新(使用从 GET 返回查得的 field.id)
58
+ curl -s -X PUT "$BASE_URL/api/docker-devices/local/config/<solution_id>" \
52
59
  -H "Content-Type: application/json" \
53
60
  -d '{
54
61
  "values": {
55
- "MQTT_BROKER_HOST": "192.168.1.200",
56
- "MQTT_BROKER_PORT": 1883
62
+ "mqtt_broker": "192.168.1.200",
63
+ "mqtt_port": 1883
57
64
  }
58
65
  }'
59
66
 
60
67
  # 远程设备实例更新
61
- curl -s -X PUT "http://127.0.0.1:3260/api/docker-devices/config/<solution_id>" \
68
+ curl -s -X PUT "$BASE_URL/api/docker-devices/config/<solution_id>" \
62
69
  -H "Content-Type: application/json" \
63
70
  -d '{
64
- "values": { "MQTT_BROKER_HOST": "192.168.1.200" },
71
+ "values": { "mqtt_broker": "192.168.1.200" },
65
72
  "connection": { "host": "<ip>", "username": "<user>", "password": "<pwd>" }
66
73
  }'
67
74
  ```
68
- - **后端自动执行**:模板替换 ➔ 生成新的 env ➔ `docker compose up -d` 重新创建受影响容器 ➔ 更新部署 manifest 记录。
75
+ - **后端自动执行**:根据 `field.id` 执行模板替换 ➔ 生成新的 env ➔ `docker compose up -d` 重新创建受影响容器 ➔ 更新部署 manifest 记录。
69
76
  - **无需整机重装**,数据卷与现有文件完全保留。
70
77
 
71
78
  ---
@@ -74,16 +81,41 @@ curl -s -X PUT "http://127.0.0.1:3260/api/docker-devices/config/<solution_id>" \
74
81
 
75
82
  当用户要求“重启服务”、“暂停运行”或“更新容器镜像”时:
76
83
 
84
+ ### 本地 Docker 部署实例(无凭据要求)
77
85
  ```bash
78
86
  # 执行 start / stop / restart / update
79
- curl -s -X POST "http://127.0.0.1:3260/api/device-management/<deployment_id>/action" \
87
+ curl -s -X POST "$BASE_URL/api/device-management/<deployment_id>/action" \
80
88
  -H "Content-Type: application/json" \
81
89
  -d '{
82
90
  "action": "restart",
83
91
  "device_id": "<step_id>"
84
92
  }'
85
93
  ```
94
+
95
+ ### 远程设备实例(必须显式携带 SSH 认证凭据)
96
+ 远程设备(Jetson / Raspberry Pi 等)通过 SSH 协议由底层执行容器编排动作。由于后端安全机制禁用了 SSH-agent 与自动公钥搜寻,且不持久化保存设备明文密码,因此远程操作**必须向用户获取并显式传入 `password` 或 `key_file`**:
97
+
98
+ ```bash
99
+ # 远程实例 - 密码认证
100
+ curl -s -X POST "$BASE_URL/api/device-management/<deployment_id>/action" \
101
+ -H "Content-Type: application/json" \
102
+ -d '{
103
+ "action": "restart",
104
+ "device_id": "<step_id>",
105
+ "password": "<ssh_password>"
106
+ }'
107
+
108
+ # 远程实例 - 私钥文件认证(支持 ~/.ssh/ 下的私钥路径)
109
+ curl -s -X POST "$BASE_URL/api/device-management/<deployment_id>/action" \
110
+ -H "Content-Type: application/json" \
111
+ -d '{
112
+ "action": "restart",
113
+ "device_id": "<step_id>",
114
+ "key_file": "~/.ssh/id_rsa"
115
+ }'
116
+ ```
86
117
  - `action` 支持:`start`(启动)、`stop`(停止)、`restart`(重启)、`update`(拉取最新镜像并重启)。
118
+ - **判定规则**:从步骤一 `/api/device-management/active` 的返回值中检查目标实例的 `host` 字段。若 `host` 不为 `localhost` 或 `127.0.0.1`,即属于远程设备,发起 action 请求前必须准备好认证凭据。
87
119
 
88
120
  ---
89
121
 
@@ -81,7 +81,7 @@ three classes:
81
81
  | Class | Example | What you do |
82
82
  |---|---|---|
83
83
  | **Device / environment** | port taken, disk full, Docker missing, network blocked, wrong password, serial permission | Propose the exact fix commands → user OK → run them → retry |
84
- | **Solution configuration** | timeout too short, wrong port/env in compose, health check endpoint 404, minor syntax/transfer glitch | **CLI / Engine mode (Auto-healing patch)**: Copy the solution from `~/.sensecraft/content/solutions/<id>` to `./.sensecraft-work/solutions/<id>`, apply the minimal required fix. Stop the previous engine instance (`sensecraft-engine daemon stop`) so it reloads the new solution directory, and redeploy with `--solutions-dir ./.sensecraft-work/solutions` so the user succeeds immediately! Show the user the patch diff. Desktop app running: fix it in the edit copy (`app-only.md` §4) → validate → apply → retry. |
84
+ | **Solution configuration** | timeout too short, wrong port/env in compose, health check endpoint 404, minor syntax/transfer glitch | **CLI / Engine mode (Auto-healing patch)**: Copy the solution from `~/.sensecraft/content/solutions/<id>` to `./.sensecraft-work/solutions/<id>`, apply the minimal required fix, and redeploy with `sensecraft-engine deploy <id> --solutions-dir ./.sensecraft-work/solutions` so the user succeeds immediately! Show the user the patch diff. Desktop app running: fix it in the edit copy (`app-only.md` §4) → validate → apply → retry. |
85
85
  | **Engine bug** | exit code 5, a panic in `engine.log`, a step that fails the same way on any device, behaviour contradicting the solution's own config | Stop. Prepare the issue report below |
86
86
 
87
87
  If the evidence does not fit any row, say so and show the user the first error
@@ -65,11 +65,8 @@ git clone https://github.com/Seeed-Studio/ModelAssistant.git ./my-model-assistan
65
65
  修改完成后,无需打包推送到 CDN,直接利用 `sensecraft-engine` 的 `--solutions-dir` 特性在本地无缝验证:
66
66
 
67
67
  ```bash
68
- # 1. 如果已有守护进程正在运行,先停止释放端口与旧方案缓存
69
- sensecraft-engine daemon stop
70
-
71
- # 2. 从本地自定义工程目录加载方案并执行部署测试
72
- sensecraft-engine deploy --solutions-dir ./my-solutions/solutions --solution <id>
68
+ # 从本地自定义工程目录加载方案并执行部署测试
69
+ sensecraft-engine deploy <id> --solutions-dir ./my-solutions/solutions
73
70
  ```
74
71
  - 引擎将直接读取本地工程中的 `solution.yaml`、`devices/*.yaml` 和 `docker-compose.yml`,秒级完成本地部署生效。
75
72