@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/knowledge.js
CHANGED
|
@@ -5,6 +5,7 @@ import https from 'node:https';
|
|
|
5
5
|
import http from 'node:http';
|
|
6
6
|
import { createCatalogDto } from './catalog-dto.js';
|
|
7
7
|
import { assertCompatibility, COMPATIBILITY_STATUS } from './compatibility.js';
|
|
8
|
+
import { getIntegrationContract } from './integration.js';
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = path.dirname(__filename);
|
|
@@ -122,6 +123,623 @@ export function loadClientConfig() {
|
|
|
122
123
|
return null;
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
const SUPPORTED_LOCALES = new Set(['en', 'zh', 'ja']);
|
|
127
|
+
const HUB_TYPES = new Set(['scenario', 'tech', 'reference_design']);
|
|
128
|
+
const HUB_SECTIONS = new Set(['overview', 'links', 'description', 'architecture', 'products', 'deployment', 'playbook', 'integration']);
|
|
129
|
+
|
|
130
|
+
function levenshteinDistance(s1, s2) {
|
|
131
|
+
const m = s1.length;
|
|
132
|
+
const n = s2.length;
|
|
133
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
134
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
135
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
136
|
+
for (let i = 1; i <= m; i++) {
|
|
137
|
+
for (let j = 1; j <= n; j++) {
|
|
138
|
+
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
139
|
+
dp[i][j] = Math.min(
|
|
140
|
+
dp[i - 1][j] + 1,
|
|
141
|
+
dp[i][j - 1] + 1,
|
|
142
|
+
dp[i - 1][j - 1] + cost
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return dp[m][n];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function findClosestSection(received, allowed = Array.from(HUB_SECTIONS)) {
|
|
150
|
+
const norm = String(received || '').toLowerCase().trim();
|
|
151
|
+
if (!norm) return null;
|
|
152
|
+
|
|
153
|
+
// 1. Prefix match (e.g. "arch" -> "architecture", "integrations" -> "integration")
|
|
154
|
+
const prefixMatch = allowed.find(s => s.startsWith(norm) || norm.startsWith(s));
|
|
155
|
+
if (prefixMatch) return prefixMatch;
|
|
156
|
+
|
|
157
|
+
// 2. Substring match
|
|
158
|
+
const substringMatch = allowed.find(s => s.includes(norm) || norm.includes(s));
|
|
159
|
+
if (substringMatch) return substringMatch;
|
|
160
|
+
|
|
161
|
+
// 3. Levenshtein edit distance
|
|
162
|
+
let best = null;
|
|
163
|
+
let minDistance = Infinity;
|
|
164
|
+
for (const s of allowed) {
|
|
165
|
+
const dist = levenshteinDistance(norm, s);
|
|
166
|
+
if (dist < minDistance) {
|
|
167
|
+
minDistance = dist;
|
|
168
|
+
best = s;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (minDistance <= 3) {
|
|
172
|
+
return best;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class HubSectionError extends Error {
|
|
178
|
+
constructor(section, allowed = Array.from(HUB_SECTIONS)) {
|
|
179
|
+
const suggestion = findClosestSection(section, allowed);
|
|
180
|
+
const msg = `Unsupported hub section "${section}". Supported: ${allowed.join(', ')}.${suggestion ? ` Did you mean "${suggestion}"?` : ''}`;
|
|
181
|
+
super(msg);
|
|
182
|
+
this.name = 'HubSectionError';
|
|
183
|
+
this.section = section;
|
|
184
|
+
this.suggestion = suggestion;
|
|
185
|
+
this.allowed_sections = allowed;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function computeSectionCapabilities(record, rowType, core = null) {
|
|
190
|
+
if (!core) core = loadKnowledgeCore();
|
|
191
|
+
const available_sections = ['overview', 'links'];
|
|
192
|
+
const section_status = {
|
|
193
|
+
overview: 'documented',
|
|
194
|
+
links: 'documented',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const selected = selectedContent(record, 'zh');
|
|
198
|
+
const content = selected.value;
|
|
199
|
+
const deploy = record?.deployable_content || { deployable: false, solution_id: null };
|
|
200
|
+
|
|
201
|
+
const hasDescDoc = deploy.description_documents && Object.keys(deploy.description_documents).length > 0;
|
|
202
|
+
const hasDescBody = Boolean(content?.description_body || content?.description || record?.summary);
|
|
203
|
+
if (hasDescDoc || hasDescBody) {
|
|
204
|
+
available_sections.push('description');
|
|
205
|
+
section_status.description = 'documented';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const hasArchOverlay = deploy.architecture_overlay && Object.keys(deploy.architecture_overlay).length > 0;
|
|
209
|
+
const hasArchModules = Boolean(content?.architecture?.length);
|
|
210
|
+
const hasGallery = Boolean(deploy.architecture_and_gallery?.length);
|
|
211
|
+
if (hasArchOverlay || hasArchModules || hasGallery) {
|
|
212
|
+
available_sections.push('architecture');
|
|
213
|
+
section_status.architecture = 'documented';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const hasProducts = (deploy.products && Object.keys(deploy.products).length > 0) || Boolean(content?.products?.length);
|
|
217
|
+
if (hasProducts) {
|
|
218
|
+
available_sections.push('products');
|
|
219
|
+
section_status.products = 'documented';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const hasGuides = deploy.deployment_guides && Object.keys(deploy.deployment_guides).length > 0;
|
|
223
|
+
const hasPresets = Boolean(deploy.presets?.length);
|
|
224
|
+
const hasGuideDocs = deploy.deployment_guide_documents && Object.keys(deploy.deployment_guide_documents).length > 0;
|
|
225
|
+
if (hasGuides || hasPresets || hasGuideDocs) {
|
|
226
|
+
available_sections.push('deployment');
|
|
227
|
+
section_status.deployment = 'documented';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let pbStatus = null;
|
|
231
|
+
if (record?.diagnostic_playbook || core.diagnostic_playbooks?.[record?.id]) {
|
|
232
|
+
pbStatus = 'documented';
|
|
233
|
+
} else if (rowType === 'reference_design') {
|
|
234
|
+
for (const scId of (record?.scenario_ids || [])) {
|
|
235
|
+
const sc = (core.scenarios || []).find((s) => s.id === scId);
|
|
236
|
+
if (sc?.diagnostic_playbook) {
|
|
237
|
+
pbStatus = 'inherited';
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else if (rowType === 'scenario') {
|
|
242
|
+
for (const rdId of (record?.reference_designs || [])) {
|
|
243
|
+
const rd = (core.reference_designs || []).find((r) => r.id === rdId);
|
|
244
|
+
if (rd?.diagnostic_playbook) {
|
|
245
|
+
pbStatus = 'inherited';
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!pbStatus && (rowType === 'scenario' || rowType === 'reference_design')) {
|
|
251
|
+
pbStatus = 'generic';
|
|
252
|
+
}
|
|
253
|
+
if (pbStatus) {
|
|
254
|
+
available_sections.push('playbook');
|
|
255
|
+
section_status.playbook = pbStatus;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const isCustomContract = ['recamera-gym-tracker', 'vision-ai-industrial-camera', 'smart-waste-sorting'].includes(deploy.solution_id || record?.id);
|
|
259
|
+
const hasIntegration = Boolean(deploy.deployable || deploy.solution_id || rowType === 'reference_design' || isCustomContract);
|
|
260
|
+
if (hasIntegration) {
|
|
261
|
+
available_sections.push('integration');
|
|
262
|
+
section_status.integration = isCustomContract ? 'documented' : 'inferred';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { available_sections, section_status };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function normalizeLocale(locale = 'zh') {
|
|
269
|
+
const value = locale === 'zh-hans' ? 'zh' : String(locale || '').toLowerCase();
|
|
270
|
+
if (!SUPPORTED_LOCALES.has(value)) throw new Error(`Unsupported locale "${locale}". Use en, zh, or ja.`);
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function visibilityAllows(record, locale, type) {
|
|
275
|
+
if (record?.published === false) return false;
|
|
276
|
+
if (type === 'reference_design') return record?.visibility !== 'staging' && record?.visibility !== 'draft';
|
|
277
|
+
const value = record?.[`visibility_${locale === 'zh' ? 'zh_hans' : locale}`] ?? record?.visibility;
|
|
278
|
+
return value !== 'staging' && value !== 'draft';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function recordType(record, fallback) {
|
|
282
|
+
if (fallback === 'reference_design') return fallback;
|
|
283
|
+
return record?.page_type === 'TECH' ? 'tech' : 'scenario';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function localizedValue(value, locale) {
|
|
287
|
+
if (!value || typeof value !== 'object') return value || '';
|
|
288
|
+
return value[locale] || value.zh || value.en || value.ja || '';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function localizedLanding(record, locale) {
|
|
292
|
+
return record?.landing_urls?.[locale] || record?.landing_url || null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function selectedContent(record, locale) {
|
|
296
|
+
const requested = record.locale_content?.[locale];
|
|
297
|
+
if (requested) return { value: requested, locale };
|
|
298
|
+
for (const fallback of ['en', 'zh', 'ja']) {
|
|
299
|
+
if (record.locale_content?.[fallback]) return { value: record.locale_content[fallback], locale: fallback };
|
|
300
|
+
}
|
|
301
|
+
return { value: null, locale: null };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function selectedSummary(record, content, requestedLocale, selectedLocale) {
|
|
305
|
+
if (typeof content?.summary === 'string' && content.summary.trim()) {
|
|
306
|
+
return { value: content.summary, locale: selectedLocale };
|
|
307
|
+
}
|
|
308
|
+
if (record?.summary && typeof record.summary === 'object' && !Array.isArray(record.summary)) {
|
|
309
|
+
for (const locale of [requestedLocale, 'en', 'zh', 'ja']) {
|
|
310
|
+
if (typeof record.summary[locale] === 'string' && record.summary[locale].trim()) {
|
|
311
|
+
return { value: record.summary[locale], locale };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { value: null, locale: null };
|
|
315
|
+
}
|
|
316
|
+
if (typeof record?.summary === 'string' && record.summary.trim()) {
|
|
317
|
+
return { value: record.summary, locale: null };
|
|
318
|
+
}
|
|
319
|
+
return { value: null, locale: null };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function compactRecord(record, type, locale) {
|
|
323
|
+
const resolvedType = recordType(record, type);
|
|
324
|
+
const visible = visibilityAllows(record, locale, type);
|
|
325
|
+
return {
|
|
326
|
+
id: record.id,
|
|
327
|
+
type: resolvedType,
|
|
328
|
+
title: localizedValue(record.title, locale) || record.name || record.id,
|
|
329
|
+
landing_url: visible ? localizedLanding(record, locale) : null,
|
|
330
|
+
scenario_ids: record.scenario_ids || [],
|
|
331
|
+
reference_designs: record.reference_designs || [],
|
|
332
|
+
technology_ids: record.technology_ids || [],
|
|
333
|
+
solution_id: record.deployable_content?.solution_id || null,
|
|
334
|
+
deployable: record.deployable_content?.deployable === true,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function compactSearchRecord(record) {
|
|
339
|
+
const { diagnostic_playbook: pb, ...compact } = record;
|
|
340
|
+
const hasPlaybook = Boolean(pb && (pb.questions?.length || pb.core_dimensions?.length));
|
|
341
|
+
const core = loadKnowledgeCore();
|
|
342
|
+
const bundled = (record.type === 'reference_design')
|
|
343
|
+
? (core.reference_designs || []).find((r) => r.id === record.id)
|
|
344
|
+
: (core.scenarios || []).find((s) => s.id === record.id);
|
|
345
|
+
const targetRecord = bundled || record;
|
|
346
|
+
const caps = computeSectionCapabilities(targetRecord, record.type, core);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
...compact,
|
|
350
|
+
summary: compact.summary || compact.subtitle?.zh || '',
|
|
351
|
+
available_sections: caps.available_sections,
|
|
352
|
+
section_status: caps.section_status,
|
|
353
|
+
...(hasPlaybook ? {
|
|
354
|
+
has_playbook: true,
|
|
355
|
+
playbook_dimensions: pb.core_dimensions || [],
|
|
356
|
+
} : {}),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function localizedSearch(record, locale) {
|
|
361
|
+
const selected = selectedContent(record, locale);
|
|
362
|
+
const contentTitle = typeof selected.value?.title === 'string' && selected.value.title.trim() ? selected.value.title : '';
|
|
363
|
+
let titleLocale = null;
|
|
364
|
+
let title = '';
|
|
365
|
+
if (contentTitle && selected.locale === locale) {
|
|
366
|
+
titleLocale = selected.locale;
|
|
367
|
+
title = contentTitle;
|
|
368
|
+
} else {
|
|
369
|
+
titleLocale = [locale, 'en', 'zh', 'ja'].find((key) => typeof record?.title?.[key] === 'string' && record.title[key].trim()) || null;
|
|
370
|
+
title = titleLocale ? record.title[titleLocale] : contentTitle;
|
|
371
|
+
if (!titleLocale && contentTitle) titleLocale = selected.locale;
|
|
372
|
+
}
|
|
373
|
+
const summary = selectedSummary(record, selected.value, locale, selected.locale);
|
|
374
|
+
const contentLocale = summary.value ? summary.locale : titleLocale;
|
|
375
|
+
return {
|
|
376
|
+
title,
|
|
377
|
+
title_locale: titleLocale,
|
|
378
|
+
summary: summary.value || '',
|
|
379
|
+
content_locale: contentLocale,
|
|
380
|
+
fallback: contentLocale !== null && contentLocale !== locale,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function onlineLocalizedText(record, locale, fieldsByLocale) {
|
|
385
|
+
const fallbackLocales = locale === 'ja'
|
|
386
|
+
? ['en', 'zh']
|
|
387
|
+
: locale === 'en'
|
|
388
|
+
? ['zh', 'ja']
|
|
389
|
+
: ['en', 'ja'];
|
|
390
|
+
for (const candidateLocale of [locale, ...fallbackLocales]) {
|
|
391
|
+
for (const field of fieldsByLocale[candidateLocale] || []) {
|
|
392
|
+
const value = record?.[field];
|
|
393
|
+
if (typeof value === 'string' && value.trim()) return { value, locale: candidateLocale };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return { value: '', locale: null };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function extractArchitecturePreview(doc, bundled, locale, type) {
|
|
400
|
+
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
401
|
+
if (type === 'reference_design') {
|
|
402
|
+
const candidateIntro = doc?.[`architecture_intro_md_${locKey}`] ||
|
|
403
|
+
doc?.architecture_intro_md_zh_hans ||
|
|
404
|
+
doc?.architecture_intro_md_en ||
|
|
405
|
+
doc?.architecture_intro_md_ja;
|
|
406
|
+
if (typeof candidateIntro === 'string') {
|
|
407
|
+
const match = candidateIntro.match(/!\[(.*?)\]\(([^\)]+)\)/);
|
|
408
|
+
if (match) {
|
|
409
|
+
return {
|
|
410
|
+
image_url: match[2],
|
|
411
|
+
summary: match[1] || doc?.[`architecture_lead_${locKey}`] || '',
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const overlay = bundled?.deployable_content?.architecture_overlay;
|
|
416
|
+
if (overlay) {
|
|
417
|
+
const o = overlay[locale] || overlay.zh || overlay.en || overlay.ja;
|
|
418
|
+
if (typeof o?.intro === 'string') {
|
|
419
|
+
const match = o.intro.match(/!\[(.*?)\]\(([^\)]+)\)/);
|
|
420
|
+
if (match) {
|
|
421
|
+
return {
|
|
422
|
+
image_url: match[2],
|
|
423
|
+
summary: match[1] || '',
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} else if (type === 'scenario') {
|
|
429
|
+
const pageDataCandidate = doc?.[`page_data_${locKey}`] || doc?.page_data_zh_hans || doc?.page_data_en;
|
|
430
|
+
if (pageDataCandidate) {
|
|
431
|
+
try {
|
|
432
|
+
const pd = typeof pageDataCandidate === 'string' ? JSON.parse(pageDataCandidate) : pageDataCandidate;
|
|
433
|
+
const tabs = pd?.deployment?.tabs || [];
|
|
434
|
+
for (const tab of tabs) {
|
|
435
|
+
if (typeof tab.card_image === 'string' && tab.card_image.trim()) {
|
|
436
|
+
return {
|
|
437
|
+
image_url: tab.card_image.trim(),
|
|
438
|
+
summary: tab.label || tab.title || tab.id || '',
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch (e) {}
|
|
443
|
+
}
|
|
444
|
+
const locContent = bundled?.locale_content?.[locale] || bundled?.locale_content?.zh || bundled?.locale_content?.en;
|
|
445
|
+
const archTabs = locContent?.architecture || [];
|
|
446
|
+
for (const tab of archTabs) {
|
|
447
|
+
if (typeof tab.card_image === 'string' && tab.card_image.trim()) {
|
|
448
|
+
return {
|
|
449
|
+
image_url: tab.card_image.trim(),
|
|
450
|
+
summary: tab.label || tab.id || '',
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function bundledSearchProjection(record, type, locale, source = 'bundled_manifest') {
|
|
459
|
+
const text = localizedSearch(record, locale);
|
|
460
|
+
const archPreview = extractArchitecturePreview(null, record, locale, type);
|
|
461
|
+
return {
|
|
462
|
+
type,
|
|
463
|
+
id: record.id,
|
|
464
|
+
title: text.title,
|
|
465
|
+
title_locale: text.title_locale,
|
|
466
|
+
summary: text.summary,
|
|
467
|
+
content_locale: text.content_locale,
|
|
468
|
+
fallback: text.fallback,
|
|
469
|
+
...(archPreview ? { architecture_preview: archPreview } : {}),
|
|
470
|
+
landing_url: appendUtm(localizedLanding(record, locale), {
|
|
471
|
+
source: 'sensecraft_agent', medium: 'solution_consult', campaign: record.id, content: 'solution_landing',
|
|
472
|
+
}),
|
|
473
|
+
scenario_ids: record.scenario_ids || [],
|
|
474
|
+
technology_ids: record.technology_ids || [],
|
|
475
|
+
reference_designs: record.reference_designs || [],
|
|
476
|
+
details_available: true,
|
|
477
|
+
next_action: `Use hub get ${record.id} --type ${type} for verified section content.`,
|
|
478
|
+
source,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function allHubRecords(locale, { includeHidden = false } = {}) {
|
|
483
|
+
const core = loadKnowledgeCore();
|
|
484
|
+
const scenarios = (core.scenarios || []).filter(r => includeHidden || visibilityAllows(r, locale, 'scenario'))
|
|
485
|
+
.map(r => ({ record: r, type: recordType(r, 'scenario') }));
|
|
486
|
+
const refs = (core.reference_designs || []).filter(r => includeHidden || visibilityAllows(r, locale, 'reference_design'))
|
|
487
|
+
.map(r => ({ record: r, type: 'reference_design' }));
|
|
488
|
+
return [...scenarios, ...refs].sort((a, b) => `${a.type}:${a.record.id}`.localeCompare(`${b.type}:${b.record.id}`));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** List the complete bundled page snapshot, with compact rows only. */
|
|
492
|
+
export function listHub({ type = null, locale = 'zh', page = 1, limit = 50 } = {}) {
|
|
493
|
+
const loc = normalizeLocale(locale);
|
|
494
|
+
if (type && !HUB_TYPES.has(type)) throw new Error(`Unsupported hub type "${type}".`);
|
|
495
|
+
if (!Number.isSafeInteger(page) || page < 1) throw new Error('Page must be a positive integer.');
|
|
496
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new Error('Limit must be an integer from 1 to 200.');
|
|
497
|
+
const records = allHubRecords(loc).filter(({ type: rowType }) => !type || rowType === type);
|
|
498
|
+
const start = (page - 1) * limit;
|
|
499
|
+
const rows = records.slice(start, start + limit).map(({ record, type: rowType }) => compactRecord(record, rowType, loc));
|
|
500
|
+
const total = records.length;
|
|
501
|
+
return createCatalogDto({
|
|
502
|
+
source: 'bundled_manifest',
|
|
503
|
+
freshness: 'UNKNOWN',
|
|
504
|
+
datasetVersion: loadKnowledgeCore().dataset_version,
|
|
505
|
+
records: rows,
|
|
506
|
+
locale: loc,
|
|
507
|
+
scope: 'bundled_catalog_snapshot',
|
|
508
|
+
pagination: { page, limit, total, returned: rows.length, has_more: start + rows.length < total, next_page: start + rows.length < total ? page + 1 : null },
|
|
509
|
+
provenance: loadKnowledgeCore().provenance || null,
|
|
510
|
+
warnings: ['Snapshot completeness applies to the bundled catalog only; it is not a live website inventory.'],
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function populateSectionData(row, section, record, rowType, loc, selected, content, summary, deploy, core, preset) {
|
|
515
|
+
if (section === 'links') {
|
|
516
|
+
row.links = { landing: row.landing_urls, wiki: record.wiki_url || null, repo: record.repo_url || null, declared_documents: deploy.source_files || [] };
|
|
517
|
+
} else if (section === 'description') {
|
|
518
|
+
const doc = deploy.description_documents?.[loc] || deploy.description_documents?.en || null;
|
|
519
|
+
const body = doc?.body || content?.description_body || content?.description || summary.value || '';
|
|
520
|
+
const descriptionLocale = doc?.content_locale || (body === summary.value ? summary.locale : selected.locale);
|
|
521
|
+
row.description = {
|
|
522
|
+
content_locale: descriptionLocale,
|
|
523
|
+
fallback: descriptionLocale !== null && descriptionLocale !== loc,
|
|
524
|
+
source_role: doc ? 'deployment_document' : (content?.description_source_role || 'summary'),
|
|
525
|
+
body,
|
|
526
|
+
source: doc?.source || content?.source || null,
|
|
527
|
+
sha256: doc?.sha256 || null,
|
|
528
|
+
file: deploy.description_files?.[loc] || deploy.description_files?.en || null,
|
|
529
|
+
};
|
|
530
|
+
} else if (section === 'architecture') {
|
|
531
|
+
const overlayLocales = deploy.architecture_overlay || {};
|
|
532
|
+
const architecture = overlayLocales[loc]
|
|
533
|
+
? { value: overlayLocales[loc], locale: loc }
|
|
534
|
+
: (overlayLocales.en ? { value: overlayLocales.en, locale: 'en' }
|
|
535
|
+
: (overlayLocales.zh ? { value: overlayLocales.zh, locale: 'zh' }
|
|
536
|
+
: (overlayLocales.ja ? { value: overlayLocales.ja, locale: 'ja' } : { value: null, locale: null })));
|
|
537
|
+
const galleryLocale = (deploy.architecture_and_gallery || []).some((g) => g.src_i18n?.[loc]) ? loc : ((deploy.architecture_and_gallery || []).some((g) => g.src_i18n?.en) ? 'en' : null);
|
|
538
|
+
const overlay = architecture.value;
|
|
539
|
+
const archPreview = extractArchitecturePreview(null, record, loc, rowType);
|
|
540
|
+
row.architecture = {
|
|
541
|
+
content_locale: architecture.locale || (content?.content_locale ? selected.locale : galleryLocale),
|
|
542
|
+
fallback: Boolean(architecture.locale && architecture.locale !== loc),
|
|
543
|
+
source_role: overlay ? 'reference_design.overlay' : (content?.architecture?.length ? 'deployment.tabs' : 'solution.gallery'),
|
|
544
|
+
image_url: archPreview?.image_url || null,
|
|
545
|
+
summary: archPreview?.summary || null,
|
|
546
|
+
intro: overlay?.intro || '',
|
|
547
|
+
modules: overlay?.tabs || content?.architecture || [],
|
|
548
|
+
source: overlay?.source || null,
|
|
549
|
+
gallery: deploy.architecture_and_gallery || [],
|
|
550
|
+
};
|
|
551
|
+
} else if (section === 'products') {
|
|
552
|
+
row.products = deploy.products || content?.products || {};
|
|
553
|
+
row.bom = content?.bom || deploy.bom || {};
|
|
554
|
+
row.products_content_locale = content?.products?.length ? selected.locale : null;
|
|
555
|
+
row.products_source_role = content?.products?.length ? 'deployment.tabs' : 'solution.device_catalog';
|
|
556
|
+
} else if (section === 'deployment') {
|
|
557
|
+
const guide = deploy.deployment_guide_documents?.[loc] || deploy.deployment_guide_documents?.en || null;
|
|
558
|
+
let guideBody = guide ? guide.body : null;
|
|
559
|
+
let filteredByPreset = null;
|
|
560
|
+
|
|
561
|
+
if (guideBody && preset) {
|
|
562
|
+
const normPreset = String(preset).toLowerCase().trim();
|
|
563
|
+
const packagePattern = /(^|\n)##\s+套餐[^\n]*\{#([a-zA-Z0-9_\-]+)\}/g;
|
|
564
|
+
let match;
|
|
565
|
+
const sections = [];
|
|
566
|
+
while ((match = packagePattern.exec(guideBody)) !== null) {
|
|
567
|
+
sections.push({
|
|
568
|
+
id: match[2].toLowerCase(),
|
|
569
|
+
index: match.index,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
if (sections.length > 0) {
|
|
573
|
+
const targetIdx = sections.findIndex((s) => s.id === normPreset || s.id.includes(normPreset) || normPreset.includes(s.id));
|
|
574
|
+
if (targetIdx !== -1) {
|
|
575
|
+
const start = sections[targetIdx].index;
|
|
576
|
+
const end = targetIdx + 1 < sections.length ? sections[targetIdx + 1].index : guideBody.length;
|
|
577
|
+
const header = guideBody.slice(0, sections[0].index).trim();
|
|
578
|
+
const bodyChunk = guideBody.slice(start, end).trim();
|
|
579
|
+
guideBody = `${header ? header + '\n\n---\n\n' : ''}${bodyChunk}`;
|
|
580
|
+
filteredByPreset = normPreset;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
row.deployment = {
|
|
586
|
+
deployable: deploy.deployable === true,
|
|
587
|
+
solution_id: deploy.solution_id || null,
|
|
588
|
+
presets: deploy.presets || [],
|
|
589
|
+
guides: deploy.deployment_guides || {},
|
|
590
|
+
guide: guide ? { content_locale: guide.content_locale, body: guideBody, source: guide.source, sha256: guide.sha256 } : null,
|
|
591
|
+
source_files: deploy.source_files || [],
|
|
592
|
+
...(filteredByPreset ? { filtered_by_preset: filteredByPreset } : {}),
|
|
593
|
+
};
|
|
594
|
+
} else if (section === 'playbook') {
|
|
595
|
+
let pb = record.diagnostic_playbook || core.diagnostic_playbooks?.[record.id];
|
|
596
|
+
if (!pb && rowType === 'reference_design') {
|
|
597
|
+
for (const scId of (record.scenario_ids || [])) {
|
|
598
|
+
const sc = (core.scenarios || []).find((s) => s.id === scId);
|
|
599
|
+
if (sc?.diagnostic_playbook) {
|
|
600
|
+
pb = sc.diagnostic_playbook;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (!pb && rowType === 'scenario') {
|
|
606
|
+
for (const rdId of (record.reference_designs || [])) {
|
|
607
|
+
const rd = (core.reference_designs || []).find((r) => r.id === rdId);
|
|
608
|
+
if (rd?.diagnostic_playbook) {
|
|
609
|
+
pb = rd.diagnostic_playbook;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (pb) {
|
|
615
|
+
row.playbook = {
|
|
616
|
+
domain: pb.domain || row.title,
|
|
617
|
+
core_dimensions: pb.core_dimensions || [],
|
|
618
|
+
questions: (pb.questions || []).map((q) => ({
|
|
619
|
+
dimension: q.dimension,
|
|
620
|
+
question: q.question,
|
|
621
|
+
rationale: q.rationale,
|
|
622
|
+
})),
|
|
623
|
+
source_role: 'diagnostic_playbook',
|
|
624
|
+
};
|
|
625
|
+
} else {
|
|
626
|
+
row.playbook = {
|
|
627
|
+
domain: row.title,
|
|
628
|
+
core_dimensions: [
|
|
629
|
+
'物理拓扑分布 (集中 vs 分布)',
|
|
630
|
+
'存量资产利旧 (摄像头/传感器)',
|
|
631
|
+
'供电与网络环境 (市电/电池/PoE)',
|
|
632
|
+
'业务执行与闭环联动 (看板/控制)',
|
|
633
|
+
],
|
|
634
|
+
questions: [
|
|
635
|
+
{ dimension: 'topology', question: '测点是在同一建筑/车间集中分布,还是分散在多个物理区域?', rationale: '决定采用单台高算力计算盒集中拉流,还是采用分布式独立一体化终端。' },
|
|
636
|
+
{ dimension: 'asset_reuse', question: '现场是否已有现成的 IP 摄像头 (RTSP/ONVIF) 或传感器 (RS485/Modbus) 可以利旧?', rationale: '直接复用存量硬件可显著缩减项目落地成本与施工周期。' },
|
|
637
|
+
{ dimension: 'power_network', question: '安装点是否有 220V 市电供电?现场网络是有线网 (PoE)、Wi-Fi 还是需 4G/LoRaWAN 远距无线?', rationale: '决定供电设备规格与通信选型。' },
|
|
638
|
+
],
|
|
639
|
+
source_role: 'generic_diagnostic_playbook',
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
} else if (section === 'integration') {
|
|
643
|
+
const guideDoc = deploy.deployment_guide_documents?.[loc] || deploy.deployment_guide_documents?.en || null;
|
|
644
|
+
const contract = getIntegrationContract(deploy.solution_id || record.id, {
|
|
645
|
+
guideText: guideDoc?.body || '',
|
|
646
|
+
});
|
|
647
|
+
row.integration = contract;
|
|
648
|
+
} else if (section === 'overview') {
|
|
649
|
+
row.links = { landing: row.landing_urls, wiki: record.wiki_url || null, repo: record.repo_url || null };
|
|
650
|
+
row.description = { content_locale: summary.locale, fallback: summary.locale !== null && summary.locale !== loc, source_role: 'summary', summary: summary.value || '' };
|
|
651
|
+
row.relationships = { scenario_ids: record.scenario_ids || [], reference_designs: record.reference_designs || [], technology_ids: record.technology_ids || [] };
|
|
652
|
+
row.deployable_content = { deployable: deploy.deployable === true, solution_id: deploy.solution_id || null, presets: deploy.presets || [], guides: deploy.deployment_guides || {} };
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Get one selected graph record and progressive content section(s). */
|
|
657
|
+
export function getHubRecord(id, { type = null, locale = 'zh', section = null, sections = null, preset = null } = {}) {
|
|
658
|
+
const loc = normalizeLocale(locale);
|
|
659
|
+
if (!id) throw new Error('A hub record ID is required.');
|
|
660
|
+
if (type && !HUB_TYPES.has(type)) throw new Error(`Unsupported hub type "${type}".`);
|
|
661
|
+
|
|
662
|
+
let requestedSections = [];
|
|
663
|
+
if (sections) {
|
|
664
|
+
if (Array.isArray(sections)) {
|
|
665
|
+
requestedSections = sections.flatMap((s) => String(s).split(','));
|
|
666
|
+
} else if (typeof sections === 'string') {
|
|
667
|
+
requestedSections = sections.split(',');
|
|
668
|
+
}
|
|
669
|
+
} else if (section) {
|
|
670
|
+
requestedSections = String(section).split(',');
|
|
671
|
+
} else {
|
|
672
|
+
requestedSections = ['overview'];
|
|
673
|
+
}
|
|
674
|
+
requestedSections = requestedSections.map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
675
|
+
if (requestedSections.length === 0) requestedSections = ['overview'];
|
|
676
|
+
|
|
677
|
+
for (const sec of requestedSections) {
|
|
678
|
+
if (!HUB_SECTIONS.has(sec)) {
|
|
679
|
+
throw new HubSectionError(sec, Array.from(HUB_SECTIONS));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const matches = allHubRecords(loc, { includeHidden: true }).filter(({ record, type: rowType }) => record.id === id && (!type || rowType === type));
|
|
684
|
+
if (matches.length === 0) return null;
|
|
685
|
+
if (matches.length > 1 && !type) throw new Error(`ID "${id}" is ambiguous; specify --type scenario, tech, or reference_design.`);
|
|
686
|
+
const { record, type: rowType } = matches[0];
|
|
687
|
+
const row = compactRecord(record, rowType, loc);
|
|
688
|
+
row.visibility = record.visibility || record[`visibility_${loc === 'zh' ? 'zh_hans' : loc}`] || null;
|
|
689
|
+
row.landing_urls = Object.fromEntries(Object.entries(record.landing_urls || {})
|
|
690
|
+
.filter(([landingLocale]) => visibilityAllows(record, landingLocale, rowType)));
|
|
691
|
+
const selected = selectedContent(record, loc);
|
|
692
|
+
const content = selected.value;
|
|
693
|
+
const summary = selectedSummary(record, content, loc, selected.locale);
|
|
694
|
+
const deploy = record.deployable_content || { deployable: false, solution_id: null };
|
|
695
|
+
const core = loadKnowledgeCore();
|
|
696
|
+
|
|
697
|
+
const caps = computeSectionCapabilities(record, rowType, core);
|
|
698
|
+
row.available_sections = caps.available_sections;
|
|
699
|
+
row.section_status = caps.section_status;
|
|
700
|
+
|
|
701
|
+
if (requestedSections.length === 1) {
|
|
702
|
+
row.section = requestedSections[0];
|
|
703
|
+
populateSectionData(row, requestedSections[0], record, rowType, loc, selected, content, summary, deploy, core, preset);
|
|
704
|
+
} else {
|
|
705
|
+
row.sections = requestedSections;
|
|
706
|
+
for (const sec of requestedSections) {
|
|
707
|
+
populateSectionData(row, sec, record, rowType, loc, selected, content, summary, deploy, core, preset);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return row;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Detects if a search query matches known hardware product families, models, or SKUs.
|
|
716
|
+
*
|
|
717
|
+
* @param {string[]} tokens
|
|
718
|
+
* @param {string} query
|
|
719
|
+
* @param {Object} core
|
|
720
|
+
* @returns {string|null} Helpful redirect note if hardware is detected
|
|
721
|
+
*/
|
|
722
|
+
function detectHardwareIntent(tokens, query, core) {
|
|
723
|
+
if (!tokens || tokens.length === 0) return null;
|
|
724
|
+
const families = Object.values(core?.product_families || {});
|
|
725
|
+
const matchedHw = families.find((fam) => {
|
|
726
|
+
const fid = (fam.id || '').toLowerCase();
|
|
727
|
+
const zh = (fam.title?.zh || '').toLowerCase();
|
|
728
|
+
const en = (fam.title?.en || '').toLowerCase();
|
|
729
|
+
if (tokens.some((t) => fid.includes(t) || zh.includes(t) || en.includes(t))) return true;
|
|
730
|
+
return (fam.skus || []).some((s) => {
|
|
731
|
+
const sNum = String(s.sku || '').toLowerCase();
|
|
732
|
+
const sModel = String(s.model || '').toLowerCase();
|
|
733
|
+
const sName = String(s.name || '').toLowerCase();
|
|
734
|
+
return tokens.some((t) => sNum === t || (sModel && sModel.includes(t)) || sName.includes(t));
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
if (matchedHw) {
|
|
738
|
+
return `💡 提示: 检测到硬件设备/型号关键词 "${query}"。若需查询该硬件的物理接口、包装清单(是否带电源)或官方落地应用方案,请使用: solution-cli product search "${query}"`;
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
|
|
125
743
|
/**
|
|
126
744
|
* Searches Seeed Solutions Hub scenarios and reference designs.
|
|
127
745
|
*
|
|
@@ -132,7 +750,11 @@ export function loadClientConfig() {
|
|
|
132
750
|
* @returns {Promise<Object>} Standardized Catalog DTO
|
|
133
751
|
*/
|
|
134
752
|
export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
753
|
+
const normalizedLocale = normalizeLocale(locale);
|
|
754
|
+
locale = normalizedLocale;
|
|
755
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new Error('Limit must be an integer from 1 to 200.');
|
|
135
756
|
const qLower = (query || '').toLowerCase().trim();
|
|
757
|
+
const tokens = qLower.split(/\s+/).filter(Boolean);
|
|
136
758
|
const clientConfig = loadClientConfig();
|
|
137
759
|
const host = process.env.TYPESENSE_HOST || clientConfig?.typesense?.host || 'https://rel-search.seeedstudio.com';
|
|
138
760
|
const keySolution = process.env.TYPESENSE_SEARCH_KEY_SOLUTION || process.env.TYPESENSE_SEARCH_KEY || clientConfig?.typesense?.search_key_solution;
|
|
@@ -180,25 +802,41 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
180
802
|
if (doc[`visibility_${locKey}`] === 'staging' || doc[`visibility_${locKey}`] === 'draft') continue;
|
|
181
803
|
|
|
182
804
|
const docId = doc.id || doc.slug;
|
|
183
|
-
const docType = doc.doc_type || 'scenario';
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
:
|
|
190
|
-
|
|
805
|
+
const docType = doc.doc_type || recordType((core.scenarios || []).find((s) => s.id === docId), 'scenario');
|
|
806
|
+
const titleText = onlineLocalizedText(doc, locale, {
|
|
807
|
+
en: ['title_en'], zh: ['title_zh_hans'], ja: ['title_ja'],
|
|
808
|
+
});
|
|
809
|
+
const summaryText = onlineLocalizedText(doc, locale, {
|
|
810
|
+
en: ['excerpt_en', 'seo_description_en'],
|
|
811
|
+
zh: ['excerpt_zh_hans', 'seo_description_zh_hans'],
|
|
812
|
+
ja: ['excerpt_ja', 'seo_description_ja'],
|
|
813
|
+
});
|
|
814
|
+
const title = titleText.value || docId;
|
|
815
|
+
const summary = summaryText.value;
|
|
816
|
+
const tags = (locale === 'en' ? doc.display_tags_en : locale === 'ja' ? (doc.display_tags_ja || doc.display_tags_en) : doc.display_tags_zh_hans) || doc.display_tags || [];
|
|
817
|
+
const bundledScenario = (core.scenarios || []).find((s) => s.id === docId);
|
|
818
|
+
const resolvedType = ['scenario', 'tech', 'reference_design'].includes(docType) ? docType : recordType(bundledScenario, 'scenario');
|
|
191
819
|
const pb = core.diagnostic_playbooks?.[docId] || doc.diagnostic_playbook || null;
|
|
192
|
-
const rawLanding = doc.external_url || doc.landing_url ||
|
|
820
|
+
const rawLanding = doc.external_url || doc.landing_url || localizedLanding(bundledScenario, locale) || null;
|
|
821
|
+
const archPreview = extractArchitecturePreview(doc, bundledScenario, locale, 'scenario');
|
|
193
822
|
|
|
194
823
|
onlineScenarios.push({
|
|
195
|
-
type:
|
|
824
|
+
type: resolvedType,
|
|
196
825
|
id: docId,
|
|
197
826
|
title,
|
|
198
827
|
summary,
|
|
199
828
|
tags,
|
|
200
829
|
family_ids: doc.family_ids || [],
|
|
201
830
|
reference_designs: doc.reference_designs || [],
|
|
831
|
+
technology_ids: doc.technology_ids || bundledScenario?.technology_ids || [],
|
|
832
|
+
scenario_ids: doc.scenario_ids || bundledScenario?.scenario_ids || [],
|
|
833
|
+
title_locale: titleText.locale,
|
|
834
|
+
summary_locale: summaryText.locale,
|
|
835
|
+
content_locale: summaryText.locale || titleText.locale,
|
|
836
|
+
fallback: (summaryText.locale || titleText.locale) !== null && (summaryText.locale || titleText.locale) !== locale,
|
|
837
|
+
...(archPreview ? { architecture_preview: archPreview } : {}),
|
|
838
|
+
details_available: Boolean(bundledScenario),
|
|
839
|
+
next_action: bundledScenario ? `Use hub get ${docId} --type ${resolvedType} for verified section content.` : `Open the production landing page for ${docId}; bundled section content is unavailable.`,
|
|
202
840
|
landing_url: appendUtm(rawLanding, {
|
|
203
841
|
source: 'sensecraft_agent',
|
|
204
842
|
medium: 'solution_consult',
|
|
@@ -218,12 +856,16 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
218
856
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
219
857
|
if (doc[`visibility_${locKey}`] === 'staging' || doc[`visibility_${locKey}`] === 'draft') continue;
|
|
220
858
|
const docId = doc.id;
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
:
|
|
859
|
+
const titleText = onlineLocalizedText(doc, locale, {
|
|
860
|
+
en: ['name_en'], zh: ['name_zh_hans'], ja: ['name_ja'],
|
|
861
|
+
});
|
|
862
|
+
const summaryText = onlineLocalizedText(doc, locale, {
|
|
863
|
+
en: ['summary_en', 'tagline_en'],
|
|
864
|
+
zh: ['summary_zh_hans', 'tagline_zh_hans'],
|
|
865
|
+
ja: ['summary_ja', 'tagline_ja'],
|
|
866
|
+
});
|
|
867
|
+
const title = titleText.value || docId;
|
|
868
|
+
const summary = summaryText.value;
|
|
227
869
|
|
|
228
870
|
let pb = doc.diagnostic_playbook || core.diagnostic_playbooks?.[docId] || null;
|
|
229
871
|
if (!pb) {
|
|
@@ -246,6 +888,7 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
246
888
|
|
|
247
889
|
const bundledRd = (core.reference_designs || []).find((r) => r.id === docId);
|
|
248
890
|
const repoUrl = doc.repo_url || bundledRd?.repo_url || '';
|
|
891
|
+
const archPreview = extractArchitecturePreview(doc, bundledRd, locale, 'reference_design');
|
|
249
892
|
|
|
250
893
|
onlineRefDesigns.push({
|
|
251
894
|
type: 'reference_design',
|
|
@@ -253,7 +896,7 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
253
896
|
title,
|
|
254
897
|
summary,
|
|
255
898
|
presets,
|
|
256
|
-
landing_url: appendUtm(
|
|
899
|
+
landing_url: appendUtm(doc.landing_url?.[locale] || doc.external_url?.[locale] || bundledRd?.landing_urls?.[locale] || null, {
|
|
257
900
|
source: 'sensecraft_agent',
|
|
258
901
|
medium: 'solution_consult',
|
|
259
902
|
campaign: docId,
|
|
@@ -265,6 +908,15 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
265
908
|
campaign: docId,
|
|
266
909
|
}) : '',
|
|
267
910
|
repo_url: repoUrl,
|
|
911
|
+
title_locale: titleText.locale,
|
|
912
|
+
summary_locale: summaryText.locale,
|
|
913
|
+
content_locale: summaryText.locale || titleText.locale,
|
|
914
|
+
fallback: (summaryText.locale || titleText.locale) !== null && (summaryText.locale || titleText.locale) !== locale,
|
|
915
|
+
...(archPreview ? { architecture_preview: archPreview } : {}),
|
|
916
|
+
scenario_ids: bundledRd?.scenario_ids || [],
|
|
917
|
+
technology_ids: bundledRd?.technology_ids || [],
|
|
918
|
+
details_available: Boolean(bundledRd),
|
|
919
|
+
next_action: bundledRd ? `Use hub get ${docId} --type reference_design for verified section content.` : `Open the production landing page for ${docId}; bundled section content is unavailable.`,
|
|
268
920
|
diagnostic_playbook: pb,
|
|
269
921
|
source: 'typesense',
|
|
270
922
|
});
|
|
@@ -274,34 +926,17 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
274
926
|
|
|
275
927
|
// If one collection returned results but the other was unavailable/failed/empty, supplement from bundled catalog
|
|
276
928
|
if ((onlineScenarios.length > 0 && onlineRefDesigns.length === 0) || (onlineRefDesigns.length > 0 && onlineScenarios.length === 0)) {
|
|
277
|
-
const tokens = qLower.split(/\s+/).filter(Boolean);
|
|
278
929
|
if (onlineScenarios.length === 0) {
|
|
279
930
|
for (const s of core.scenarios || []) {
|
|
280
931
|
if (s.published === false || s.visibility === 'staging' || s.visibility === 'draft') continue;
|
|
281
932
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
282
933
|
if (s[`visibility_${locKey}`] === 'staging' || s[`visibility_${locKey}`] === 'draft') continue;
|
|
283
934
|
|
|
284
|
-
const
|
|
935
|
+
const localizedFields = Object.values(s.locale_content || {}).flatMap((content) => [content?.title, content?.summary]);
|
|
936
|
+
const haystack = [s.id, s.title?.zh, s.title?.en, s.title?.ja, s.subtitle?.zh, s.subtitle?.en, s.subtitle?.ja, s.summary, ...localizedFields, ...(s.tags || [])].join(' ').toLowerCase();
|
|
285
937
|
const score = tokens.reduce((acc, t) => (haystack.includes(t) ? acc + 1 : acc), 0);
|
|
286
938
|
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
|
-
});
|
|
939
|
+
onlineScenarios.push({ ...bundledSearchProjection({ ...s, summary: s.subtitle || s.summary }, recordType(s, 'scenario'), locale), tags: s.tags || [], family_ids: s.family_ids || [], repo_url: s.repo_url || '', diagnostic_playbook: s.diagnostic_playbook || null });
|
|
305
940
|
}
|
|
306
941
|
}
|
|
307
942
|
}
|
|
@@ -311,30 +946,11 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
311
946
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
312
947
|
if (rd[`visibility_${locKey}`] === 'staging' || rd[`visibility_${locKey}`] === 'draft') continue;
|
|
313
948
|
|
|
314
|
-
const
|
|
949
|
+
const localizedFields = Object.values(rd.locale_content || {}).flatMap((content) => [content?.title, content?.summary]);
|
|
950
|
+
const haystack = [rd.id, rd.title?.zh, rd.title?.en, rd.title?.ja, rd.summary?.zh, rd.summary?.en, rd.summary?.ja, ...localizedFields, ...(rd.presets || [])].join(' ').toLowerCase();
|
|
315
951
|
const score = tokens.reduce((acc, t) => (haystack.includes(t) ? acc + 1 : acc), 0);
|
|
316
952
|
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
|
-
});
|
|
953
|
+
onlineRefDesigns.push({ ...bundledSearchProjection(rd, 'reference_design', locale), presets: rd.presets || [], wiki_url: rd.wiki_url || '', repo_url: rd.repo_url || '', diagnostic_playbook: rd.diagnostic_playbook || null });
|
|
338
954
|
}
|
|
339
955
|
}
|
|
340
956
|
}
|
|
@@ -352,10 +968,18 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
352
968
|
}
|
|
353
969
|
|
|
354
970
|
if (mergedRecords.length > 0) {
|
|
971
|
+
const onlineWarnings = ['Online search results are partial and do not replace the bundled catalog; use hub get for verified section content.'];
|
|
972
|
+
const hwHint = detectHardwareIntent(tokens, query, core);
|
|
973
|
+
if (hwHint) {
|
|
974
|
+
onlineWarnings.push(hwHint);
|
|
975
|
+
}
|
|
355
976
|
return createCatalogDto({
|
|
356
977
|
source: (onlineScenarios.some(r => r.source === 'typesense') || onlineRefDesigns.some(r => r.source === 'typesense')) ? 'typesense' : 'bundled_manifest',
|
|
357
|
-
freshness: '
|
|
358
|
-
|
|
978
|
+
freshness: 'UNKNOWN',
|
|
979
|
+
datasetVersion: core.dataset_version,
|
|
980
|
+
scope: 'online_partial_with_bundled_fallback',
|
|
981
|
+
warnings: onlineWarnings,
|
|
982
|
+
records: mergedRecords.map(compactSearchRecord),
|
|
359
983
|
});
|
|
360
984
|
}
|
|
361
985
|
} catch {
|
|
@@ -368,39 +992,31 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
368
992
|
const matchedScenarios = [];
|
|
369
993
|
const matchedRefDesigns = [];
|
|
370
994
|
|
|
371
|
-
const tokens = qLower.split(/\s+/).filter(Boolean);
|
|
372
|
-
|
|
373
995
|
for (const s of core.scenarios || []) {
|
|
374
996
|
if (s.published === false || s.visibility === 'staging' || s.visibility === 'draft') continue;
|
|
375
997
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
376
998
|
if (s[`visibility_${locKey}`] === 'staging' || s[`visibility_${locKey}`] === 'draft') continue;
|
|
377
999
|
|
|
1000
|
+
const localizedFields = Object.values(s.locale_content || {}).flatMap((content) => [content?.title, content?.summary]);
|
|
378
1001
|
const haystack = [
|
|
379
1002
|
s.id,
|
|
380
1003
|
s.title?.zh,
|
|
381
1004
|
s.title?.en,
|
|
1005
|
+
s.title?.ja,
|
|
382
1006
|
s.subtitle?.zh,
|
|
383
1007
|
s.subtitle?.en,
|
|
1008
|
+
s.subtitle?.ja,
|
|
384
1009
|
s.summary,
|
|
1010
|
+
...localizedFields,
|
|
385
1011
|
...(s.tags || []),
|
|
386
1012
|
].join(' ').toLowerCase();
|
|
387
1013
|
|
|
388
1014
|
const score = tokens.reduce((acc, token) => (haystack.includes(token) ? acc + 1 : acc), 0);
|
|
389
1015
|
if (tokens.length === 0 || score > 0) {
|
|
390
1016
|
matchedScenarios.push({
|
|
391
|
-
|
|
392
|
-
id: s.id,
|
|
393
|
-
title: s.title?.[locale] || s.title?.zh || s.title?.en || s.id,
|
|
394
|
-
summary: s.subtitle?.[locale] || s.subtitle?.zh || s.summary || '',
|
|
1017
|
+
...bundledSearchProjection({ ...s, summary: s.subtitle || s.summary }, recordType(s, 'scenario'), locale),
|
|
395
1018
|
tags: s.tags || [],
|
|
396
1019
|
family_ids: s.family_ids || [],
|
|
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
1020
|
repo_url: s.repo_url || '',
|
|
405
1021
|
diagnostic_playbook: s.diagnostic_playbook || null,
|
|
406
1022
|
score,
|
|
@@ -413,29 +1029,24 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
413
1029
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
414
1030
|
if (rd[`visibility_${locKey}`] === 'staging' || rd[`visibility_${locKey}`] === 'draft') continue;
|
|
415
1031
|
|
|
1032
|
+
const localizedFields = Object.values(rd.locale_content || {}).flatMap((content) => [content?.title, content?.summary]);
|
|
416
1033
|
const haystack = [
|
|
417
1034
|
rd.id,
|
|
418
1035
|
rd.title?.zh,
|
|
419
1036
|
rd.title?.en,
|
|
1037
|
+
rd.title?.ja,
|
|
420
1038
|
rd.summary?.zh,
|
|
421
1039
|
rd.summary?.en,
|
|
1040
|
+
rd.summary?.ja,
|
|
1041
|
+
...localizedFields,
|
|
422
1042
|
...(rd.presets || []),
|
|
423
1043
|
].join(' ').toLowerCase();
|
|
424
1044
|
|
|
425
1045
|
const score = tokens.reduce((acc, token) => (haystack.includes(token) ? acc + 1 : acc), 0);
|
|
426
1046
|
if (tokens.length === 0 || score > 0) {
|
|
427
1047
|
matchedRefDesigns.push({
|
|
428
|
-
|
|
429
|
-
id: rd.id,
|
|
430
|
-
title: rd.title?.[locale] || rd.title?.zh || rd.title?.en || rd.id,
|
|
431
|
-
summary: rd.summary?.[locale] || rd.summary?.zh || rd.summary?.en || '',
|
|
1048
|
+
...bundledSearchProjection(rd, 'reference_design', locale),
|
|
432
1049
|
presets: rd.presets || [],
|
|
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
1050
|
wiki_url: rd.wiki_url ? appendUtm(rd.wiki_url, {
|
|
440
1051
|
source: 'sensecraft_agent',
|
|
441
1052
|
medium: 'tech_wiki',
|
|
@@ -463,13 +1074,25 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
463
1074
|
}
|
|
464
1075
|
}
|
|
465
1076
|
|
|
1077
|
+
const warnings = process.env.SENSECRAFT_OFFLINE_ONLY
|
|
1078
|
+
? ['Offline-only mode: displaying the verified bundled catalog; no online request was attempted.']
|
|
1079
|
+
: ((keySolution || keyRefDesign)
|
|
1080
|
+
? ['Online Typesense request timed out or returned no hits. Displaying verified local catalog.']
|
|
1081
|
+
: ['No Typesense API key configured or baked. Displaying verified local catalog.']);
|
|
1082
|
+
|
|
1083
|
+
// Detect if query looks like a hardware device, model, or SKU keyword
|
|
1084
|
+
const hwHint = detectHardwareIntent(tokens, query, core);
|
|
1085
|
+
if (hwHint && (combined.length === 0 || (combined[0] && combined[0].score < 2))) {
|
|
1086
|
+
warnings.push(hwHint);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
466
1089
|
return createCatalogDto({
|
|
467
1090
|
source: 'bundled_manifest',
|
|
468
|
-
freshness: '
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
1091
|
+
freshness: 'UNKNOWN',
|
|
1092
|
+
datasetVersion: core.dataset_version,
|
|
1093
|
+
scope: 'bundled_catalog_snapshot',
|
|
1094
|
+
records: combined.map(compactSearchRecord),
|
|
1095
|
+
warnings,
|
|
473
1096
|
});
|
|
474
1097
|
}
|
|
475
1098
|
|
|
@@ -500,8 +1123,12 @@ export function queryProductFamily(familyId, { locale = 'zh' } = {}) {
|
|
|
500
1123
|
records: [{
|
|
501
1124
|
family_id: family.id,
|
|
502
1125
|
title: family.title?.[locale] || family.title?.zh || family.title?.en || family.id,
|
|
1126
|
+
summary: family.summary || null,
|
|
503
1127
|
default_sku: family.default_sku,
|
|
504
1128
|
regions: family.regions || [],
|
|
1129
|
+
interfaces: family.interfaces || [],
|
|
1130
|
+
protocols: family.protocols || [],
|
|
1131
|
+
supported_scenarios: family.supported_scenarios || [],
|
|
505
1132
|
skus_count: (family.skus || []).length,
|
|
506
1133
|
skus: (family.skus || []).slice(0, 10).map((s) => ({
|
|
507
1134
|
...s,
|
|
@@ -519,16 +1146,376 @@ export function queryProductFamily(familyId, { locale = 'zh' } = {}) {
|
|
|
519
1146
|
});
|
|
520
1147
|
}
|
|
521
1148
|
|
|
1149
|
+
/**
|
|
1150
|
+
* Searches product families and member SKUs by keywords, model name, interface, or SKU number.
|
|
1151
|
+
*
|
|
1152
|
+
* @param {Object} options
|
|
1153
|
+
* @param {string} options.query - Free text query (e.g. "J4012 Super", "RS485", "114110314")
|
|
1154
|
+
* @param {'zh' | 'en' | 'ja'} [options.locale='zh']
|
|
1155
|
+
* @param {number} [options.limit=5]
|
|
1156
|
+
* @returns {Object} Catalog DTO
|
|
1157
|
+
*/
|
|
1158
|
+
export function searchProducts({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
1159
|
+
const core = loadKnowledgeCore();
|
|
1160
|
+
const families = Object.values(core.product_families || {});
|
|
1161
|
+
const qLower = (query || '').toLowerCase().trim();
|
|
1162
|
+
const tokens = qLower.split(/\s+/).filter(Boolean);
|
|
1163
|
+
|
|
1164
|
+
if (tokens.length === 0) {
|
|
1165
|
+
return createCatalogDto({
|
|
1166
|
+
source: 'bundled_manifest',
|
|
1167
|
+
freshness: 'FRESH',
|
|
1168
|
+
records: [],
|
|
1169
|
+
warnings: ['Query cannot be empty.'],
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const matches = [];
|
|
1174
|
+
|
|
1175
|
+
for (const fam of families) {
|
|
1176
|
+
let score = 0;
|
|
1177
|
+
const titleZh = (fam.title?.zh || '').toLowerCase();
|
|
1178
|
+
const titleEn = (fam.title?.en || '').toLowerCase();
|
|
1179
|
+
const fid = (fam.id || '').toLowerCase();
|
|
1180
|
+
const famHaystack = [fid, titleZh, titleEn].join(' ');
|
|
1181
|
+
|
|
1182
|
+
// Check SKU matching
|
|
1183
|
+
const matchedSkus = [];
|
|
1184
|
+
for (const sku of fam.skus || []) {
|
|
1185
|
+
const sNum = String(sku.sku || '').toLowerCase();
|
|
1186
|
+
const sModel = String(sku.model || '').toLowerCase();
|
|
1187
|
+
const sName = String(sku.name || '').toLowerCase();
|
|
1188
|
+
// Strip negative clauses like "(Super Mode NOT Supported)" from positive keyword matching
|
|
1189
|
+
const sNameClean = sName
|
|
1190
|
+
.replace(/[(\(][^)\)]*not\s+supported[^)\)]*[)\)]?/gi, '')
|
|
1191
|
+
.replace(/super\s+mode\s+not\s+supported/gi, '')
|
|
1192
|
+
.replace(/\b(?:not\s+supported|unsupported)\b/gi, '');
|
|
1193
|
+
const sAttrs = Object.values(sku.attrs || {}).map((v) => String(v).toLowerCase());
|
|
1194
|
+
const skuIfaces = (sku.specs?.connectivity?.interfaces || sku.interfaces || []).map((i) => String(i).toLowerCase());
|
|
1195
|
+
const skuProtos = (sku.specs?.connectivity?.protocols || sku.protocols || []).map((p) => String(p).toLowerCase());
|
|
1196
|
+
const skuIoPorts = (sku.specs?.io_ports || []).map((p) => String(p.type || '').toLowerCase());
|
|
1197
|
+
|
|
1198
|
+
const skuHaystack = [sNum, sModel, sNameClean, ...sAttrs, ...skuIfaces, ...skuProtos, ...skuIoPorts].join(' ');
|
|
1199
|
+
const isSingleSkuLookup = tokens.length === 1 && (sNum === tokens[0] || sNum === qLower);
|
|
1200
|
+
const skuMatchesAll = tokens.every((t) => skuHaystack.includes(t));
|
|
1201
|
+
const singleTokenHit = tokens.length === 1 && ((sModel && sModel.includes(tokens[0])) || skuHaystack.includes(tokens[0]));
|
|
1202
|
+
|
|
1203
|
+
if (isSingleSkuLookup || skuMatchesAll) {
|
|
1204
|
+
matchedSkus.push(sku);
|
|
1205
|
+
} else if (tokens.length > 1) {
|
|
1206
|
+
const combinedMatchesAll = tokens.every((t) => (famHaystack + ' ' + skuHaystack).includes(t));
|
|
1207
|
+
const nonFamTokens = tokens.filter((t) => !famHaystack.includes(t));
|
|
1208
|
+
if (combinedMatchesAll && (nonFamTokens.length === 0 || nonFamTokens.every((t) => skuHaystack.includes(t)))) {
|
|
1209
|
+
matchedSkus.push(sku);
|
|
1210
|
+
}
|
|
1211
|
+
} else if (singleTokenHit) {
|
|
1212
|
+
matchedSkus.push(sku);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Require all query tokens to be satisfied either in the family or across member SKUs
|
|
1217
|
+
const famMatchesAll = tokens.every((t) => famHaystack.includes(t));
|
|
1218
|
+
const allTokensSatisfiedInFamily = tokens.every((t) => {
|
|
1219
|
+
return famHaystack.includes(t) || (fam.skus || []).some((s) => {
|
|
1220
|
+
const sNameClean = String(s.name || '')
|
|
1221
|
+
.toLowerCase()
|
|
1222
|
+
.replace(/[(\(][^)\)]*not\s+supported[^)\)]*[)\)]?/gi, '')
|
|
1223
|
+
.replace(/super\s+mode\s+not\s+supported/gi, '');
|
|
1224
|
+
const sIfaces = (s.specs?.connectivity?.interfaces || s.interfaces || []).map((i) => String(i).toLowerCase());
|
|
1225
|
+
const sProtos = (s.specs?.connectivity?.protocols || s.protocols || []).map((p) => String(p).toLowerCase());
|
|
1226
|
+
const sPorts = (s.specs?.io_ports || []).map((p) => String(p.type || '').toLowerCase());
|
|
1227
|
+
return [s.sku, s.model, sNameClean, ...Object.values(s.attrs || {}), ...sIfaces, ...sProtos, ...sPorts].join(' ').toLowerCase().includes(t);
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
if (!allTokensSatisfiedInFamily) {
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (matchedSkus.length > 0 || famMatchesAll) {
|
|
1236
|
+
score = matchedSkus.length * 10 + (famMatchesAll ? 5 : 0);
|
|
1237
|
+
matches.push({
|
|
1238
|
+
family: fam,
|
|
1239
|
+
score,
|
|
1240
|
+
matchedSkus: matchedSkus.length > 0 ? matchedSkus : (fam.skus || []).slice(0, 3),
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
matches.sort((a, b) => b.score - a.score);
|
|
1246
|
+
|
|
1247
|
+
const records = matches.slice(0, limit).map(({ family, matchedSkus }) => {
|
|
1248
|
+
return {
|
|
1249
|
+
family_id: family.id,
|
|
1250
|
+
title: family.title?.[locale] || family.title?.zh || family.title?.en || family.id,
|
|
1251
|
+
summary: family.summary || null,
|
|
1252
|
+
default_sku: family.default_sku,
|
|
1253
|
+
interfaces: family.interfaces || [],
|
|
1254
|
+
protocols: family.protocols || [],
|
|
1255
|
+
supported_scenarios: family.supported_scenarios || [],
|
|
1256
|
+
matched_skus: matchedSkus.map((s) => {
|
|
1257
|
+
const specs = s.specs || {};
|
|
1258
|
+
const pwr = specs.power || {};
|
|
1259
|
+
const pkg = specs.package_contents || [];
|
|
1260
|
+
const pkgText = pkg.join(' ').toLowerCase();
|
|
1261
|
+
|
|
1262
|
+
// Require positive evidence of an included operational battery (not just battery_powered capability or RTC battery)
|
|
1263
|
+
const hasIncludedBattery = pkg.some((item) => {
|
|
1264
|
+
if (!/\bbattery\b/i.test(item)) return false;
|
|
1265
|
+
if (/(?:not\s+included|sold\s+separately|cr1220|rtc|coin)/i.test(item)) return false;
|
|
1266
|
+
if (/(?:without|w\/o|no|exclude[ds]?)\s+[^()]*\bbattery/i.test(item)) return false;
|
|
1267
|
+
return true;
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
let adapterIncluded = pwr.adapter_included;
|
|
1271
|
+
if (adapterIncluded === undefined || adapterIncluded === null) {
|
|
1272
|
+
if (s.attrs?.power_adapter === 'not_included' || /w\/?o\s+power/i.test(s.name)) {
|
|
1273
|
+
adapterIncluded = false;
|
|
1274
|
+
} else if (s.attrs?.power_adapter === 'included' || /power\s+adapter/i.test(pkgText)) {
|
|
1275
|
+
adapterIncluded = true;
|
|
1276
|
+
} else {
|
|
1277
|
+
adapterIncluded = hasIncludedBattery ? false : null;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const cordExplicitlyExcluded =
|
|
1282
|
+
s.attrs?.power_adapter === 'included_no_cord' ||
|
|
1283
|
+
s.attrs?.power_cord === 'not_included' ||
|
|
1284
|
+
/without\s+power\s+cord/i.test(pkgText) ||
|
|
1285
|
+
/no\s+power\s+cord/i.test(pkgText) ||
|
|
1286
|
+
/w\/?o\s+power\s+cord/i.test(pkgText) ||
|
|
1287
|
+
/power\s+cord\s+(?:sold\s+separately|not\s+included|optional|excluded)/i.test(pkgText) ||
|
|
1288
|
+
/without\s+power\s+cord/i.test(pwr.adapter_spec || '') ||
|
|
1289
|
+
/without\s+power\s+cord/i.test(s.name || '');
|
|
1290
|
+
|
|
1291
|
+
let powerCordIncluded = null;
|
|
1292
|
+
if (cordExplicitlyExcluded) {
|
|
1293
|
+
powerCordIncluded = false;
|
|
1294
|
+
} else if (s.attrs?.power_cord === 'included') {
|
|
1295
|
+
powerCordIncluded = true;
|
|
1296
|
+
} else if (pkg.length > 0) {
|
|
1297
|
+
const hasExplicitCord = pkg.some((item) => {
|
|
1298
|
+
const itemLower = item.toLowerCase();
|
|
1299
|
+
return /power\s+cord/i.test(itemLower) &&
|
|
1300
|
+
!/(?:without|sold\s+separately|not\s+included|optional|no\s+|w\/o|separately)/i.test(itemLower);
|
|
1301
|
+
});
|
|
1302
|
+
if (hasExplicitCord) {
|
|
1303
|
+
powerCordIncluded = true;
|
|
1304
|
+
} else if (adapterIncluded === true) {
|
|
1305
|
+
const isIntegratedPlug =
|
|
1306
|
+
/universal|wall\s*mount|plug/i.test(pwr.adapter_spec || '') ||
|
|
1307
|
+
/universal\s+power\s+supply/i.test(pkgText) ||
|
|
1308
|
+
/(?:with\s+[^()]*|\b(?:interchangeable|multi-country|global)\s+)plugs?\b/i.test(pkgText);
|
|
1309
|
+
if (isIntegratedPlug) {
|
|
1310
|
+
powerCordIncluded = true;
|
|
1311
|
+
} else {
|
|
1312
|
+
powerCordIncluded = null;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
let outOfTheBoxReady = false;
|
|
1318
|
+
let readinessSummary = '';
|
|
1319
|
+
|
|
1320
|
+
const isDevBoard = /xiao|seeeduino|arduino/i.test(family.id) || /board|development\s+kit/i.test(s.name) || (pwr.input_voltage && /3\.7|5v/i.test(pwr.input_voltage));
|
|
1321
|
+
|
|
1322
|
+
if (hasIncludedBattery) {
|
|
1323
|
+
outOfTheBoxReady = true;
|
|
1324
|
+
readinessSummary = `随附配套电池供电(${pwr.input_connector ? '支持 ' + pwr.input_connector : '支持太阳能/USB充电'}),开箱即可直接上电运行。`;
|
|
1325
|
+
} else if (adapterIncluded === false) {
|
|
1326
|
+
outOfTheBoxReady = false;
|
|
1327
|
+
if (isDevBoard) {
|
|
1328
|
+
readinessSummary = `开发板/套件(仅含主板与配件),需通过 ${pwr.input_connector || 'USB-C'} 供电或自备外接电源/电池。`;
|
|
1329
|
+
} else {
|
|
1330
|
+
readinessSummary = `仅含主机,不随附电源适配器。用户买回去无法直接开机,必须加配 ${pwr.adapter_spec || '合适规格电源适配器'}。`;
|
|
1331
|
+
}
|
|
1332
|
+
} else if (adapterIncluded === true) {
|
|
1333
|
+
if (powerCordIncluded === false) {
|
|
1334
|
+
outOfTheBoxReady = false;
|
|
1335
|
+
readinessSummary = `随附电源适配器(${pwr.adapter_spec || '标配'}),但不随附 AC 电源线,开箱前需自备或加配电源线。`;
|
|
1336
|
+
} else if (powerCordIncluded === true) {
|
|
1337
|
+
outOfTheBoxReady = true;
|
|
1338
|
+
readinessSummary = `随附电源适配器与电源线(或一体式插头),开箱即用。`;
|
|
1339
|
+
} else {
|
|
1340
|
+
outOfTheBoxReady = false;
|
|
1341
|
+
readinessSummary = `随附电源适配器(${pwr.adapter_spec || '标配'}),包装清单未明确电源线随附情况,请核对是否需自备电源线。`;
|
|
1342
|
+
}
|
|
1343
|
+
} else {
|
|
1344
|
+
outOfTheBoxReady = false;
|
|
1345
|
+
readinessSummary = pwr.input_voltage ? `供电规格: ${pwr.input_voltage} ${pwr.input_connector || ''},请核对是否自备电源。` : '电源随附情况未声明。';
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Get matching required accessories
|
|
1349
|
+
const requiredAccessories = (family.required_accessories || []).filter((acc) => {
|
|
1350
|
+
if (!acc.applies_when) return true;
|
|
1351
|
+
const attrVal = s.attrs?.[acc.applies_when.attr];
|
|
1352
|
+
const target = acc.applies_when.value;
|
|
1353
|
+
if (Array.isArray(target)) return target.includes(attrVal);
|
|
1354
|
+
return attrVal === target;
|
|
1355
|
+
}).map((acc) => ({
|
|
1356
|
+
id: acc.id,
|
|
1357
|
+
name: acc.name,
|
|
1358
|
+
sku: acc.sku,
|
|
1359
|
+
reason: acc.reason,
|
|
1360
|
+
url: acc.url,
|
|
1361
|
+
}));
|
|
1362
|
+
|
|
1363
|
+
return {
|
|
1364
|
+
sku: s.sku,
|
|
1365
|
+
model: s.model || null,
|
|
1366
|
+
name: s.name,
|
|
1367
|
+
url: appendUtm(s.url, {
|
|
1368
|
+
source: 'sensecraft_agent',
|
|
1369
|
+
medium: 'product_search',
|
|
1370
|
+
campaign: family.id,
|
|
1371
|
+
content: `sku_${s.sku}`,
|
|
1372
|
+
}),
|
|
1373
|
+
package_profile: {
|
|
1374
|
+
power_adapter_included: adapterIncluded,
|
|
1375
|
+
power_cord_included: powerCordIncluded,
|
|
1376
|
+
out_of_the_box_ready: outOfTheBoxReady,
|
|
1377
|
+
readiness_summary: readinessSummary,
|
|
1378
|
+
},
|
|
1379
|
+
required_accessories: requiredAccessories,
|
|
1380
|
+
power: {
|
|
1381
|
+
adapter_included: adapterIncluded,
|
|
1382
|
+
adapter_spec: pwr.adapter_spec || null,
|
|
1383
|
+
input_voltage: pwr.input_voltage || null,
|
|
1384
|
+
input_connector: pwr.input_connector || null,
|
|
1385
|
+
},
|
|
1386
|
+
package_contents: pkg,
|
|
1387
|
+
interfaces: specs.connectivity?.interfaces || family.interfaces || [],
|
|
1388
|
+
io_ports: specs.io_ports || [],
|
|
1389
|
+
expansion_slots: specs.expansion_slots || [],
|
|
1390
|
+
ai_performance: specs.ai_performance || null,
|
|
1391
|
+
};
|
|
1392
|
+
}),
|
|
1393
|
+
};
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
return createCatalogDto({
|
|
1397
|
+
source: 'bundled_manifest',
|
|
1398
|
+
freshness: 'FRESH',
|
|
1399
|
+
records,
|
|
1400
|
+
warnings: records.length === 0 ? [`未找到与 "${query}" 匹配的硬件产品或 SKU。`] : [],
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
|
|
522
1404
|
/**
|
|
523
1405
|
* Queries and verifies accessories for a product family, running physical compatibility checks.
|
|
524
1406
|
*
|
|
525
1407
|
* @param {string} familyId
|
|
526
1408
|
* @param {Object} [options]
|
|
1409
|
+
/**
|
|
1410
|
+
* Bazaar bulk-add-cart supports simple numeric SKUs.
|
|
1411
|
+
*
|
|
1412
|
+
* @param {string|null|undefined} sku
|
|
1413
|
+
* @returns {boolean}
|
|
1414
|
+
*/
|
|
1415
|
+
export function isCartableSku(sku) {
|
|
1416
|
+
if (!sku) return false;
|
|
1417
|
+
return /^\d+$/.test(String(sku).trim());
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Storefront domain per site locale (ja -> jp.seeedstudio.com, else www.seeedstudio.com).
|
|
1422
|
+
*
|
|
1423
|
+
* @param {string} [locale='en']
|
|
1424
|
+
* @returns {string}
|
|
1425
|
+
*/
|
|
1426
|
+
export function storeBaseUrlForLocale(locale) {
|
|
1427
|
+
return locale === 'ja'
|
|
1428
|
+
? 'https://jp.seeedstudio.com'
|
|
1429
|
+
: 'https://www.seeedstudio.com';
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* Chinese locale does not check out on global store.
|
|
1434
|
+
*
|
|
1435
|
+
* @param {string} [locale]
|
|
1436
|
+
* @returns {boolean}
|
|
1437
|
+
*/
|
|
1438
|
+
export function localeSupportsCart(locale) {
|
|
1439
|
+
return locale !== 'zh-hans' && locale !== 'zh';
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Builds Seeed Bazaar bulk-add-cart deep link with UTM attribution.
|
|
1444
|
+
*
|
|
1445
|
+
* @param {Array<{sku: string, qty: number}>} items
|
|
1446
|
+
* @param {Object} [options]
|
|
1447
|
+
* @param {string} [options.solutionSlug='solution']
|
|
1448
|
+
* @param {string} [options.locale='en']
|
|
1449
|
+
* @param {string} [options.requestId]
|
|
1450
|
+
* @returns {string|null}
|
|
1451
|
+
*/
|
|
1452
|
+
export function buildBulkAddCartUrl(items, { solutionSlug = 'solution', locale = 'en', requestId = null } = {}) {
|
|
1453
|
+
const validItems = (items || [])
|
|
1454
|
+
.filter((it) => it && isCartableSku(it.sku) && Number.isInteger(it.qty) && it.qty > 0)
|
|
1455
|
+
.map((it) => `${it.sku}:${it.qty}`);
|
|
1456
|
+
|
|
1457
|
+
if (validItems.length === 0) return null;
|
|
1458
|
+
if (!localeSupportsCart(locale)) return null;
|
|
1459
|
+
|
|
1460
|
+
const reqId = requestId || `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1461
|
+
const medium = encodeURIComponent(`Seeed::Solution::${solutionSlug}::sample-purchase`);
|
|
1462
|
+
const baseUrl = storeBaseUrlForLocale(locale);
|
|
1463
|
+
const rawUrl = `${baseUrl}/bulk-add-cart?items=${validItems.join(',')}&requestId=${encodeURIComponent(reqId)}&c_event_medium=${medium}`;
|
|
1464
|
+
|
|
1465
|
+
return appendUtm(rawUrl, {
|
|
1466
|
+
source: 'sensecraft_agent',
|
|
1467
|
+
medium: 'solution_bom',
|
|
1468
|
+
campaign: solutionSlug,
|
|
1469
|
+
content: 'sample_purchase',
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Evaluates whether an accessory with applies_when condition matches the selected host SKU.
|
|
1475
|
+
*
|
|
1476
|
+
* @param {Object} acc - Accessory definition
|
|
1477
|
+
* @param {string} hostSku - Selected host SKU
|
|
1478
|
+
* @param {Object} family - Product family object
|
|
1479
|
+
* @returns {boolean}
|
|
1480
|
+
*/
|
|
1481
|
+
export function isAccessoryApplicable(acc, hostSku, family) {
|
|
1482
|
+
if (!acc || !acc.applies_when) {
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
const { attr, value } = acc.applies_when;
|
|
1486
|
+
if (!attr) return true;
|
|
1487
|
+
|
|
1488
|
+
const skuObj = (family?.skus || []).find((s) => s.sku === hostSku);
|
|
1489
|
+
const actualValue = skuObj?.attrs?.[attr];
|
|
1490
|
+
|
|
1491
|
+
if (actualValue === undefined || actualValue === null) {
|
|
1492
|
+
const fallbackValue = family?.[attr] ?? family?.physical_spec?.[attr];
|
|
1493
|
+
if (fallbackValue !== undefined && fallbackValue !== null) {
|
|
1494
|
+
return Array.isArray(value) ? value.includes(fallbackValue) : value === fallbackValue;
|
|
1495
|
+
}
|
|
1496
|
+
if (skuObj && skuObj.attrs) {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (Array.isArray(value)) {
|
|
1503
|
+
return value.includes(actualValue);
|
|
1504
|
+
}
|
|
1505
|
+
return value === actualValue;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Queries accessories for a given product family and evaluates compatibility.
|
|
1510
|
+
*
|
|
1511
|
+
* @param {string} familyId - Product family ID
|
|
1512
|
+
* @param {Object} [options]
|
|
527
1513
|
* @param {string} [options.sku] - Specific host SKU
|
|
528
1514
|
* @param {string} [options.locale='zh']
|
|
1515
|
+
* @param {boolean} [options.compact=false] - Return token-efficient compact payload
|
|
529
1516
|
* @returns {Object} Catalog DTO with structured, verified accessories
|
|
530
1517
|
*/
|
|
531
|
-
export function queryAccessories(familyId, { sku = null, locale = 'zh' } = {}) {
|
|
1518
|
+
export function queryAccessories(familyId, { sku = null, locale = 'zh', compact = false, category = null } = {}) {
|
|
532
1519
|
const core = loadKnowledgeCore();
|
|
533
1520
|
const family = core.product_families?.[familyId];
|
|
534
1521
|
|
|
@@ -560,11 +1547,13 @@ export function queryAccessories(familyId, { sku = null, locale = 'zh' } = {}) {
|
|
|
560
1547
|
qty: acc.qty || 1,
|
|
561
1548
|
reason: acc.reason || 'Required for standard operation',
|
|
562
1549
|
applies_when: acc.applies_when || null,
|
|
563
|
-
compatibility:
|
|
564
|
-
is_compatible: comp.isCompatible,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1550
|
+
compatibility: compact
|
|
1551
|
+
? { is_compatible: comp.isCompatible, status: comp.status }
|
|
1552
|
+
: {
|
|
1553
|
+
is_compatible: comp.isCompatible,
|
|
1554
|
+
status: comp.status,
|
|
1555
|
+
details: comp.dimensions,
|
|
1556
|
+
},
|
|
568
1557
|
};
|
|
569
1558
|
});
|
|
570
1559
|
|
|
@@ -583,25 +1572,73 @@ export function queryAccessories(familyId, { sku = null, locale = 'zh' } = {}) {
|
|
|
583
1572
|
content: `optional_${acc.id}`,
|
|
584
1573
|
}),
|
|
585
1574
|
type: acc.type || 'peripheral',
|
|
586
|
-
compatibility:
|
|
587
|
-
is_compatible: comp.isCompatible,
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1575
|
+
compatibility: compact
|
|
1576
|
+
? { is_compatible: comp.isCompatible, status: comp.status }
|
|
1577
|
+
: {
|
|
1578
|
+
is_compatible: comp.isCompatible,
|
|
1579
|
+
status: comp.status,
|
|
1580
|
+
details: comp.dimensions,
|
|
1581
|
+
},
|
|
591
1582
|
};
|
|
592
1583
|
});
|
|
593
1584
|
|
|
1585
|
+
let filteredRequired = required;
|
|
1586
|
+
let filteredOptional = optional;
|
|
1587
|
+
|
|
1588
|
+
if (category) {
|
|
1589
|
+
const catTokens = String(category).toLowerCase().split(',').map((s) => s.trim()).filter(Boolean);
|
|
1590
|
+
if (catTokens.length > 0) {
|
|
1591
|
+
filteredRequired = required.filter((acc) => {
|
|
1592
|
+
if (catTokens.includes('required')) return true;
|
|
1593
|
+
if (catTokens.includes('optional')) return false;
|
|
1594
|
+
const txt = [acc.id, acc.name, acc.sku, acc.category, acc.reason].join(' ').toLowerCase();
|
|
1595
|
+
return catTokens.some((tok) => txt.includes(tok));
|
|
1596
|
+
});
|
|
1597
|
+
filteredOptional = optional.filter((acc) => {
|
|
1598
|
+
if (catTokens.includes('optional')) return true;
|
|
1599
|
+
if (catTokens.includes('required')) return false;
|
|
1600
|
+
const txt = [acc.id, acc.name, acc.sku, acc.category, acc.type].join(' ').toLowerCase();
|
|
1601
|
+
return catTokens.some((tok) => txt.includes(tok));
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const hostSku = sku || family.default_sku;
|
|
1607
|
+
const cartItems = [];
|
|
1608
|
+
if (isCartableSku(hostSku)) {
|
|
1609
|
+
cartItems.push({ sku: hostSku, qty: 1 });
|
|
1610
|
+
}
|
|
1611
|
+
for (const req of filteredRequired) {
|
|
1612
|
+
const isApplicable = isAccessoryApplicable(req, hostSku, family);
|
|
1613
|
+
req.applicable = isApplicable;
|
|
1614
|
+
if (isApplicable && isCartableSku(req.sku)) {
|
|
1615
|
+
cartItems.push({ sku: req.sku, qty: req.qty || 1 });
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const bundleCartUrl = buildBulkAddCartUrl(cartItems, {
|
|
1620
|
+
solutionSlug: family.id,
|
|
1621
|
+
locale,
|
|
1622
|
+
});
|
|
1623
|
+
|
|
594
1624
|
return createCatalogDto({
|
|
595
1625
|
source: 'bundled_manifest',
|
|
596
1626
|
freshness: 'FRESH',
|
|
597
1627
|
records: [{
|
|
598
1628
|
family_id: family.id,
|
|
599
1629
|
host_title: family.title?.[locale] || family.title?.zh || family.title?.en,
|
|
600
|
-
host_sku:
|
|
1630
|
+
host_sku: hostSku,
|
|
601
1631
|
physical_spec: hostPhysicalSpec,
|
|
1632
|
+
bundle_cart_url: bundleCartUrl,
|
|
1633
|
+
...(bundleCartUrl ? {} : {
|
|
1634
|
+
domestic_channel_note: locale === 'zh'
|
|
1635
|
+
? '国内采购建议通过 Seeed 官方直营店铺或授权分销商采购(非国际站美金结算)'
|
|
1636
|
+
: null,
|
|
1637
|
+
}),
|
|
1638
|
+
...(category ? { filtered_by_category: category } : {}),
|
|
602
1639
|
accessories: {
|
|
603
|
-
required,
|
|
604
|
-
optional,
|
|
1640
|
+
required: filteredRequired,
|
|
1641
|
+
optional: filteredOptional,
|
|
605
1642
|
},
|
|
606
1643
|
}],
|
|
607
1644
|
});
|