@seeed-studio/solution-cli 0.2.0 → 0.3.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/lib/knowledge.js CHANGED
@@ -12,6 +12,44 @@ const DATA_FILE = path.join(__dirname, '..', 'catalog', 'knowledge-core.json');
12
12
 
13
13
  let cachedKnowledgeCore = null;
14
14
 
15
+ /**
16
+ * Appends UTM tracking parameters to a URL while preserving existing query parameters.
17
+ *
18
+ * @param {string} urlStr - Target URL
19
+ * @param {Object} utmParams
20
+ * @param {string} [utmParams.source='sensecraft_agent']
21
+ * @param {string} [utmParams.medium='solution_consult']
22
+ * @param {string} [utmParams.campaign='']
23
+ * @param {string} [utmParams.content='']
24
+ * @returns {string} URL with appended UTM parameters
25
+ */
26
+ export function appendUtm(urlStr, {
27
+ source = 'sensecraft_agent',
28
+ medium = 'solution_consult',
29
+ campaign = '',
30
+ content = '',
31
+ } = {}) {
32
+ if (!urlStr || typeof urlStr !== 'string') return urlStr || '';
33
+ try {
34
+ const url = new URL(urlStr);
35
+ if (!url.searchParams.has('utm_source') && source) {
36
+ url.searchParams.set('utm_source', source);
37
+ }
38
+ if (!url.searchParams.has('utm_medium') && medium) {
39
+ url.searchParams.set('utm_medium', medium);
40
+ }
41
+ if (!url.searchParams.has('utm_campaign') && campaign) {
42
+ url.searchParams.set('utm_campaign', campaign);
43
+ }
44
+ if (!url.searchParams.has('utm_content') && content) {
45
+ url.searchParams.set('utm_content', content);
46
+ }
47
+ return url.toString();
48
+ } catch {
49
+ return urlStr;
50
+ }
51
+ }
52
+
15
53
  /**
16
54
  * Loads the bundled offline knowledge core file.
17
55
  *
@@ -62,6 +100,28 @@ function fetchJson(urlStr, headers = {}, timeoutMs = 2500) {
62
100
  });
63
101
  }
64
102
 
103
+ const CONFIG_FILE = path.resolve(__dirname, '../catalog/client-config.json');
104
+ let cachedClientConfig = null;
105
+
106
+ /**
107
+ * Loads build-time baked client configuration if present.
108
+ *
109
+ * @returns {Object|null}
110
+ */
111
+ export function loadClientConfig() {
112
+ if (cachedClientConfig) return cachedClientConfig;
113
+ try {
114
+ if (fs.existsSync(CONFIG_FILE)) {
115
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
116
+ cachedClientConfig = JSON.parse(raw);
117
+ return cachedClientConfig;
118
+ }
119
+ } catch {
120
+ // Graceful fallback if missing or invalid
121
+ }
122
+ return null;
123
+ }
124
+
65
125
  /**
66
126
  * Searches Seeed Solutions Hub scenarios and reference designs.
67
127
  *
@@ -73,34 +133,232 @@ function fetchJson(urlStr, headers = {}, timeoutMs = 2500) {
73
133
  */
74
134
  export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
75
135
  const qLower = (query || '').toLowerCase().trim();
76
- const searchKey = process.env.SENSECRAFT_TYPESENSE_PURCHASE_SEARCH_KEY || process.env.TYPESENSE_SEARCH_KEY;
77
- const host = process.env.TYPESENSE_HOST || 'https://rel-search.seeedstudio.com';
136
+ const clientConfig = loadClientConfig();
137
+ const host = process.env.TYPESENSE_HOST || clientConfig?.typesense?.host || 'https://rel-search.seeedstudio.com';
138
+ const keySolution = process.env.TYPESENSE_SEARCH_KEY_SOLUTION || process.env.TYPESENSE_SEARCH_KEY || clientConfig?.typesense?.search_key_solution;
139
+ const keyRefDesign = process.env.TYPESENSE_SEARCH_KEY_REFERENCE_DESIGN || process.env.TYPESENSE_SEARCH_KEY || clientConfig?.typesense?.search_key_reference_design;
78
140
 
79
- // Attempt online Typesense search if key is provided and not explicitly running offline
80
- if (searchKey && !process.env.SENSECRAFT_OFFLINE_ONLY) {
141
+ // Attempt online Typesense search if keys are provided and not explicitly running offline
142
+ if ((keySolution || keyRefDesign) && !process.env.SENSECRAFT_OFFLINE_ONLY) {
81
143
  try {
82
- const searchUrl = `${host}/collections/Solution/documents/search?q=${encodeURIComponent(query)}&query_by=title,description,tags&per_page=${limit}`;
83
- const data = await fetchJson(searchUrl, { 'X-TYPESENSE-API-KEY': searchKey }, 2500);
84
- if (data && Array.isArray(data.hits)) {
85
- const records = data.hits.map((hit) => {
86
- const doc = hit.document || {};
87
- return {
88
- type: doc.doc_type || 'scenario',
89
- id: doc.id || doc.slug,
90
- title: doc.title,
91
- summary: doc.summary || doc.description,
92
- tags: doc.tags || [],
93
- family_ids: doc.family_ids || [],
94
- source: 'typesense',
95
- };
96
- });
144
+ const core = loadKnowledgeCore();
145
+ const onlinePromises = [];
146
+
147
+ const locKey = locale === 'zh' ? 'zh_hans' : locale;
148
+ const fetchLimit = Math.max(limit * 5, 30);
149
+
150
+ if (keySolution) {
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}`;
153
+ onlinePromises.push(
154
+ fetchJson(solUrl, { 'X-TYPESENSE-API-KEY': keySolution }, 2500)
155
+ .then((data) => ({ type: 'scenario', hits: data?.hits || [] }))
156
+ .catch(() => ({ type: 'scenario', hits: [] }))
157
+ );
158
+ }
159
+
160
+ if (keyRefDesign) {
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}`;
163
+ onlinePromises.push(
164
+ fetchJson(rdUrl, { 'X-TYPESENSE-API-KEY': keyRefDesign }, 2500)
165
+ .then((data) => ({ type: 'reference_design', hits: data?.hits || [] }))
166
+ .catch(() => ({ type: 'reference_design', hits: [] }))
167
+ );
168
+ }
169
+
170
+ const results = await Promise.all(onlinePromises);
171
+ const onlineScenarios = [];
172
+ const onlineRefDesigns = [];
173
+
174
+ for (const res of results) {
175
+ if (res.type === 'scenario') {
176
+ for (const hit of res.hits) {
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
+
182
+ const docId = doc.id || doc.slug;
183
+ const docType = doc.doc_type || 'scenario';
184
+ const title = locale === 'en'
185
+ ? (doc.title_en || doc.title_zh_hans || docId)
186
+ : (doc.title_zh_hans || doc.title_en || docId);
187
+ const summary = locale === 'en'
188
+ ? (doc.excerpt_en || doc.seo_description_en || doc.excerpt_zh_hans || '')
189
+ : (doc.excerpt_zh_hans || doc.seo_description_zh_hans || doc.excerpt_en || '');
190
+ const tags = (locale === 'en' ? doc.display_tags_en : doc.display_tags_zh_hans) || doc.display_tags || [];
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}`;
193
+
194
+ onlineScenarios.push({
195
+ type: docType,
196
+ id: docId,
197
+ title,
198
+ summary,
199
+ tags,
200
+ family_ids: doc.family_ids || [],
201
+ reference_designs: doc.reference_designs || [],
202
+ landing_url: appendUtm(rawLanding, {
203
+ source: 'sensecraft_agent',
204
+ medium: 'solution_consult',
205
+ campaign: docId,
206
+ content: `${docType}_landing`,
207
+ }),
208
+ repo_url: doc.repo_url || '',
209
+ diagnostic_playbook: pb,
210
+ source: 'typesense',
211
+ });
212
+ }
213
+ } else if (res.type === 'reference_design') {
214
+ for (const hit of res.hits) {
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;
220
+ const docId = doc.id;
221
+ const title = locale === 'en'
222
+ ? (doc.name_en || doc.name_zh_hans || docId)
223
+ : (doc.name_zh_hans || doc.name_en || docId);
224
+ const summary = locale === 'en'
225
+ ? (doc.summary_en || doc.tagline_en || doc.summary_zh_hans || '')
226
+ : (doc.summary_zh_hans || doc.tagline_zh_hans || doc.summary_en || '');
227
+
228
+ let pb = doc.diagnostic_playbook || core.diagnostic_playbooks?.[docId] || null;
229
+ if (!pb) {
230
+ for (const sc of core.scenarios || []) {
231
+ if ((sc.reference_designs || []).includes(docId)) {
232
+ pb = sc.diagnostic_playbook || core.diagnostic_playbooks?.[sc.id] || null;
233
+ break;
234
+ }
235
+ }
236
+ }
237
+
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({
251
+ type: 'reference_design',
252
+ id: docId,
253
+ title,
254
+ summary,
255
+ presets,
256
+ landing_url: appendUtm(`https://www.seeedstudio.com/solutions/reference-designs/${docId}`, {
257
+ source: 'sensecraft_agent',
258
+ medium: 'solution_consult',
259
+ campaign: docId,
260
+ content: 'solution_landing',
261
+ }),
262
+ wiki_url: doc.wiki_url ? appendUtm(doc.wiki_url, {
263
+ source: 'sensecraft_agent',
264
+ medium: 'tech_wiki',
265
+ campaign: docId,
266
+ }) : '',
267
+ repo_url: repoUrl,
268
+ diagnostic_playbook: pb,
269
+ source: 'typesense',
270
+ });
271
+ }
272
+ }
273
+ }
274
+
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) {
97
355
  return createCatalogDto({
98
- source: 'typesense',
356
+ source: (onlineScenarios.some(r => r.source === 'typesense') || onlineRefDesigns.some(r => r.source === 'typesense')) ? 'typesense' : 'bundled_manifest',
99
357
  freshness: 'FRESH',
100
- records: records.slice(0, limit),
358
+ records: mergedRecords,
101
359
  });
102
360
  }
103
- } catch (err) {
361
+ } catch {
104
362
  // Graceful fallback to offline core with notice
105
363
  }
106
364
  }
@@ -113,6 +371,10 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
113
371
  const tokens = qLower.split(/\s+/).filter(Boolean);
114
372
 
115
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
+
116
378
  const haystack = [
117
379
  s.id,
118
380
  s.title?.zh,
@@ -133,12 +395,24 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
133
395
  tags: s.tags || [],
134
396
  family_ids: s.family_ids || [],
135
397
  reference_designs: s.reference_designs || [],
398
+ landing_url: appendUtm(s.landing_url || `https://www.seeedstudio.com/solutions/${s.id}`, {
399
+ source: 'sensecraft_agent',
400
+ medium: 'solution_consult',
401
+ campaign: s.id,
402
+ content: 'scenario_landing',
403
+ }),
404
+ repo_url: s.repo_url || '',
405
+ diagnostic_playbook: s.diagnostic_playbook || null,
136
406
  score,
137
407
  });
138
408
  }
139
409
  }
140
410
 
141
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
+
142
416
  const haystack = [
143
417
  rd.id,
144
418
  rd.title?.zh,
@@ -156,7 +430,19 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
156
430
  title: rd.title?.[locale] || rd.title?.zh || rd.title?.en || rd.id,
157
431
  summary: rd.summary?.[locale] || rd.summary?.zh || rd.summary?.en || '',
158
432
  presets: rd.presets || [],
159
- wiki_url: rd.wiki_url || '',
433
+ landing_url: appendUtm(rd.landing_url || `https://www.seeedstudio.com/solutions/reference-designs/${rd.id}`, {
434
+ source: 'sensecraft_agent',
435
+ medium: 'solution_consult',
436
+ campaign: rd.id,
437
+ content: 'solution_landing',
438
+ }),
439
+ wiki_url: rd.wiki_url ? appendUtm(rd.wiki_url, {
440
+ source: 'sensecraft_agent',
441
+ medium: 'tech_wiki',
442
+ campaign: rd.id,
443
+ }) : '',
444
+ repo_url: rd.repo_url || '',
445
+ diagnostic_playbook: rd.diagnostic_playbook || null,
160
446
  score,
161
447
  });
162
448
  }
@@ -166,15 +452,24 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
166
452
  matchedScenarios.sort((a, b) => b.score - a.score);
167
453
  matchedRefDesigns.sort((a, b) => b.score - a.score);
168
454
 
169
- 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
+ }
170
465
 
171
466
  return createCatalogDto({
172
467
  source: 'bundled_manifest',
173
468
  freshness: 'FRESH',
174
469
  records: combined,
175
- warnings: searchKey
176
- ? ['Online Typesense request timed out; fell back to local bundled manifest.']
177
- : ['No Typesense API key configured. Displaying verified local catalog.'],
470
+ warnings: (keySolution || keyRefDesign)
471
+ ? ['Online Typesense request timed out or returned no hits. Displaying verified local catalog.']
472
+ : ['No Typesense API key configured or baked. Displaying verified local catalog.'],
178
473
  });
179
474
  }
180
475
 
@@ -208,7 +503,15 @@ export function queryProductFamily(familyId, { locale = 'zh' } = {}) {
208
503
  default_sku: family.default_sku,
209
504
  regions: family.regions || [],
210
505
  skus_count: (family.skus || []).length,
211
- skus: (family.skus || []).slice(0, 10),
506
+ skus: (family.skus || []).slice(0, 10).map((s) => ({
507
+ ...s,
508
+ url: appendUtm(s.url, {
509
+ source: 'sensecraft_agent',
510
+ medium: 'bom_recommendation',
511
+ campaign: family.id,
512
+ content: `sku_${s.sku}`,
513
+ }),
514
+ })),
212
515
  physical_spec: family.physical_spec || null,
213
516
  required_accessories_count: (family.required_accessories || []).length,
214
517
  optional_accessories_count: (family.optional_accessories || []).length,
@@ -248,7 +551,12 @@ export function queryAccessories(familyId, { sku = null, locale = 'zh' } = {}) {
248
551
  id: acc.id,
249
552
  name: acc.name,
250
553
  sku: acc.sku,
251
- url: acc.url,
554
+ url: appendUtm(acc.url, {
555
+ source: 'sensecraft_agent',
556
+ medium: 'bom_recommendation',
557
+ campaign: family.id,
558
+ content: `required_${acc.id}`,
559
+ }),
252
560
  qty: acc.qty || 1,
253
561
  reason: acc.reason || 'Required for standard operation',
254
562
  applies_when: acc.applies_when || null,
@@ -268,7 +576,12 @@ export function queryAccessories(familyId, { sku = null, locale = 'zh' } = {}) {
268
576
  id: acc.id,
269
577
  name: acc.name,
270
578
  sku: acc.sku,
271
- url: acc.url,
579
+ url: appendUtm(acc.url, {
580
+ source: 'sensecraft_agent',
581
+ medium: 'bom_recommendation',
582
+ campaign: family.id,
583
+ content: `optional_${acc.id}`,
584
+ }),
272
585
  type: acc.type || 'peripheral',
273
586
  compatibility: {
274
587
  is_compatible: comp.isCompatible,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seeed-studio/solution-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Zero-friction installer and CLI runner for SenseCraft Engine and AI Agent Skills (Work Buddy, Claude Code, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "./lib/installer.js",
@@ -19,9 +19,12 @@
19
19
  "lib",
20
20
  "catalog",
21
21
  "skills",
22
+ "docs",
22
23
  "README.md"
23
24
  ],
24
25
  "scripts": {
26
+ "build": "node scripts/bake-client-config.mjs",
27
+ "prepack": "node scripts/bake-client-config.mjs",
25
28
  "test": "node --test tests/cli.test.mjs",
26
29
  "install:self": "node bin/cli.js install",
27
30
  "status": "node bin/cli.js status",
@@ -16,7 +16,9 @@ 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
+ | **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
+ | **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`. |
20
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. |
21
23
 
22
24
  Capabilities that need the **SenseCraft Solution desktop app** (editing a
@@ -76,7 +78,7 @@ daemon is a host prerequisite, not evidence that a deployment has passed.
76
78
  you checked, what you changed). Transient registry or network errors follow
77
79
  the backoff rule in `references/deploy.md` §6 and do not use up a round.
78
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):
79
- - 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.
80
82
  - If running with the Desktop App: use the app's edit copy mechanism (`references/app-only.md` §4).
81
83
  5. **Reading output:** read the CLI's JSON output directly; it is small. If a
82
84
  filter is needed, use `jq` only when it is already installed. Do not start
@@ -86,8 +88,22 @@ daemon is a host prerequisite, not evidence that a deployment has passed.
86
88
  6. **Temp files:** keep request bodies and scratch files in the current
87
89
  working directory under `./.sensecraft-work/`, never in `/tmp`. Remove the
88
90
  directory when the task ends, also after a failure.
89
-
90
- ## Step 0 — Install the engine and the solutions (every task)
91
+ 7. **Conversational tone & soft referral:** Agent is an IoT domain expert and
92
+ answers technical, business, hardware, and configuration questions directly
93
+ and thoroughly using local knowledge and tools. Do NOT dump raw link lists
94
+ or write pushy sales pitches. External links (`landing_url`, `wiki_url`,
95
+ product purchase URLs, `repo_url`) must be smoothly integrated as
96
+ supplemental reading / procurement / code exploration. All URLs generated by
97
+ `solution-cli` automatically carry silent UTM parameters
98
+ (`utm_source=sensecraft_agent`) for analytics attribution.
99
+ 8. **Proactive diagnosis & domain-specific questioning:** During solution consultation,
100
+ Agent must avoid one-way monologues. Proactively inspect the `diagnostic_playbook`
101
+ returned by `solution-cli hub search` (or cross-reference `references/consult-and-select.md` §3).
102
+ Ask 2–3 targeted domain questions explaining the engineering rationale (topology,
103
+ asset reuse, power/cabling, inspection speed/materials) to help the user converge
104
+ on the optimal hardware architecture and budget.
105
+
106
+ ## Step 0 — Install the engine and the solutions (when needed)
91
107
 
92
108
  1. Check whether it is installed: `sensecraft-engine --version`. If the command
93
109
  is not found, also try `~/.sensecraft/bin/sensecraft-engine --version`
@@ -105,14 +121,28 @@ daemon is a host prerequisite, not evidence that a deployment has passed.
105
121
  command and stop; do not install packages or start another interpreter.
106
122
  `minisign` is required to verify the signed release manifest; Unix `jq` is
107
123
  required by the shell installer to read it.
108
- 3. Not installed → install it together with the solutions (ask the user first,
109
- 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:
110
128
 
111
129
  **Recommended (Zero-friction via solution-cli, auto-detects existing app & agents)**:
112
130
  ```bash
113
- npx @seeed-studio/solution-cli install -y
131
+ npx -y @seeed-studio/solution-cli@latest install -y
114
132
  ```
115
133
 
134
+ The command checks the signed stable Engine on fresh and repeat runs, reuses
135
+ an equal or newer local version, and upgrades an older one. It preserves
136
+ modified/unregistered skills and protected symlinks. Add `--agent ...` for an
137
+ explicit agent selection or `--engine-path <path>` for a valid custom pin.
138
+ `--force` can re-download equal/older versions and overwrite existing skill
139
+ files, including user modifications; use it only when that overwrite is
140
+ intended. Protected external symlinks are not followed, and it never
141
+ downgrades a newer discovered Engine. Automatic upgrade rules start with
142
+ solution-cli 0.3.1; if npm `latest` is still 0.3.0, those rules are not live.
143
+ At this verification, 0.3.1 is pending publication. Latest/signature failure
144
+ stops before writes; offline use requires a valid explicit local Engine path.
145
+
116
146
  **Fallback (Standalone shell script when Node.js is not present)**:
117
147
  ```bash
118
148
  curl --compressed -fsSL https://sensecraft-statics.seeed.cc/solution-app/engine/install.sh | sh -s -- --with-content
@@ -146,9 +176,8 @@ daemon is a host prerequisite, not evidence that a deployment has passed.
146
176
  list` or `deploy-info` exits with code 3 when no usable resource/solution
147
177
  directory is available and tells you to run `content pull`. If a usable
148
178
  directory exists but the requested solution ID is unknown, the query exits
149
- with code 2. After `content pull` / `update`, if an engine instance is
150
- already running it keeps the solutions it loaded at start: run
151
- `sensecraft-engine daemon stop` when no deployment is running.
179
+ with code 2. After `content pull` / `update`, newly downloaded or updated
180
+ solutions are available to subsequent deployment and query commands.
152
181
  5. Device pre-checks on this computer: `sensecraft-engine doctor --json`. Each
153
182
  check has `status` (`ok` / `warn` / `fail`) and a `fix` command. Show the
154
183
  user any `fail` and its `fix`; running a `fix` changes this computer and