@seeed-studio/solution-cli 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -104
- package/bin/cli.js +410 -58
- package/bin/runner.js +1 -1
- package/catalog/client-config.json +2 -2
- package/catalog/knowledge-core.json +58452 -488
- package/docs/cli-guide.md +173 -200
- package/lib/agent.js +74 -15
- package/lib/catalog-dto.js +14 -1
- package/lib/detector.js +3 -3
- package/lib/doctor.js +19 -1
- package/lib/downloader.js +24 -0
- package/lib/gotchas.js +126 -0
- package/lib/installer.js +113 -19
- package/lib/integration.js +265 -0
- package/lib/knowledge.js +838 -100
- package/lib/ledger.js +1 -1
- package/package.json +3 -6
- package/skills/{sensecraft → solution-cli}/SKILL.md +125 -30
- package/skills/{sensecraft → solution-cli}/references/consult-and-select.md +61 -13
- package/skills/solution-cli/references/deploy.md +112 -0
- package/skills/solution-cli/references/device-management.md +298 -0
- package/skills/{sensecraft → 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/{sensecraft → solution-cli}/references/hardware-bom.md +9 -3
- package/skills/solution-cli/references/sales-feasibility.md +65 -0
- package/skills/{sensecraft → solution-cli}/references/secondary-development.md +10 -7
- package/skills/sensecraft/references/deploy.md +0 -300
- package/skills/sensecraft/references/device-management.md +0 -126
- /package/skills/{sensecraft → solution-cli}/references/app-only.md +0 -0
- /package/skills/{sensecraft → solution-cli}/references/error-catalog.md +0 -0
- /package/skills/{sensecraft → solution-cli}/references/offline.md +0 -0
- /package/skills/{sensecraft → solution-cli}/references/verify.md +0 -0
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,594 @@ 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
|
+
|
|
125
714
|
/**
|
|
126
715
|
* Searches Seeed Solutions Hub scenarios and reference designs.
|
|
127
716
|
*
|
|
@@ -132,6 +721,9 @@ export function loadClientConfig() {
|
|
|
132
721
|
* @returns {Promise<Object>} Standardized Catalog DTO
|
|
133
722
|
*/
|
|
134
723
|
export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
724
|
+
const normalizedLocale = normalizeLocale(locale);
|
|
725
|
+
locale = normalizedLocale;
|
|
726
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new Error('Limit must be an integer from 1 to 200.');
|
|
135
727
|
const qLower = (query || '').toLowerCase().trim();
|
|
136
728
|
const clientConfig = loadClientConfig();
|
|
137
729
|
const host = process.env.TYPESENSE_HOST || clientConfig?.typesense?.host || 'https://rel-search.seeedstudio.com';
|
|
@@ -180,25 +772,41 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
180
772
|
if (doc[`visibility_${locKey}`] === 'staging' || doc[`visibility_${locKey}`] === 'draft') continue;
|
|
181
773
|
|
|
182
774
|
const docId = doc.id || doc.slug;
|
|
183
|
-
const docType = doc.doc_type || 'scenario';
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
:
|
|
190
|
-
|
|
775
|
+
const docType = doc.doc_type || recordType((core.scenarios || []).find((s) => s.id === docId), 'scenario');
|
|
776
|
+
const titleText = onlineLocalizedText(doc, locale, {
|
|
777
|
+
en: ['title_en'], zh: ['title_zh_hans'], ja: ['title_ja'],
|
|
778
|
+
});
|
|
779
|
+
const summaryText = onlineLocalizedText(doc, locale, {
|
|
780
|
+
en: ['excerpt_en', 'seo_description_en'],
|
|
781
|
+
zh: ['excerpt_zh_hans', 'seo_description_zh_hans'],
|
|
782
|
+
ja: ['excerpt_ja', 'seo_description_ja'],
|
|
783
|
+
});
|
|
784
|
+
const title = titleText.value || docId;
|
|
785
|
+
const summary = summaryText.value;
|
|
786
|
+
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 || [];
|
|
787
|
+
const bundledScenario = (core.scenarios || []).find((s) => s.id === docId);
|
|
788
|
+
const resolvedType = ['scenario', 'tech', 'reference_design'].includes(docType) ? docType : recordType(bundledScenario, 'scenario');
|
|
191
789
|
const pb = core.diagnostic_playbooks?.[docId] || doc.diagnostic_playbook || null;
|
|
192
|
-
const rawLanding = doc.external_url || doc.landing_url ||
|
|
790
|
+
const rawLanding = doc.external_url || doc.landing_url || localizedLanding(bundledScenario, locale) || null;
|
|
791
|
+
const archPreview = extractArchitecturePreview(doc, bundledScenario, locale, 'scenario');
|
|
193
792
|
|
|
194
793
|
onlineScenarios.push({
|
|
195
|
-
type:
|
|
794
|
+
type: resolvedType,
|
|
196
795
|
id: docId,
|
|
197
796
|
title,
|
|
198
797
|
summary,
|
|
199
798
|
tags,
|
|
200
799
|
family_ids: doc.family_ids || [],
|
|
201
800
|
reference_designs: doc.reference_designs || [],
|
|
801
|
+
technology_ids: doc.technology_ids || bundledScenario?.technology_ids || [],
|
|
802
|
+
scenario_ids: doc.scenario_ids || bundledScenario?.scenario_ids || [],
|
|
803
|
+
title_locale: titleText.locale,
|
|
804
|
+
summary_locale: summaryText.locale,
|
|
805
|
+
content_locale: summaryText.locale || titleText.locale,
|
|
806
|
+
fallback: (summaryText.locale || titleText.locale) !== null && (summaryText.locale || titleText.locale) !== locale,
|
|
807
|
+
...(archPreview ? { architecture_preview: archPreview } : {}),
|
|
808
|
+
details_available: Boolean(bundledScenario),
|
|
809
|
+
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
810
|
landing_url: appendUtm(rawLanding, {
|
|
203
811
|
source: 'sensecraft_agent',
|
|
204
812
|
medium: 'solution_consult',
|
|
@@ -218,12 +826,16 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
218
826
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
219
827
|
if (doc[`visibility_${locKey}`] === 'staging' || doc[`visibility_${locKey}`] === 'draft') continue;
|
|
220
828
|
const docId = doc.id;
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
:
|
|
829
|
+
const titleText = onlineLocalizedText(doc, locale, {
|
|
830
|
+
en: ['name_en'], zh: ['name_zh_hans'], ja: ['name_ja'],
|
|
831
|
+
});
|
|
832
|
+
const summaryText = onlineLocalizedText(doc, locale, {
|
|
833
|
+
en: ['summary_en', 'tagline_en'],
|
|
834
|
+
zh: ['summary_zh_hans', 'tagline_zh_hans'],
|
|
835
|
+
ja: ['summary_ja', 'tagline_ja'],
|
|
836
|
+
});
|
|
837
|
+
const title = titleText.value || docId;
|
|
838
|
+
const summary = summaryText.value;
|
|
227
839
|
|
|
228
840
|
let pb = doc.diagnostic_playbook || core.diagnostic_playbooks?.[docId] || null;
|
|
229
841
|
if (!pb) {
|
|
@@ -246,6 +858,7 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
246
858
|
|
|
247
859
|
const bundledRd = (core.reference_designs || []).find((r) => r.id === docId);
|
|
248
860
|
const repoUrl = doc.repo_url || bundledRd?.repo_url || '';
|
|
861
|
+
const archPreview = extractArchitecturePreview(doc, bundledRd, locale, 'reference_design');
|
|
249
862
|
|
|
250
863
|
onlineRefDesigns.push({
|
|
251
864
|
type: 'reference_design',
|
|
@@ -253,7 +866,7 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
253
866
|
title,
|
|
254
867
|
summary,
|
|
255
868
|
presets,
|
|
256
|
-
landing_url: appendUtm(
|
|
869
|
+
landing_url: appendUtm(doc.landing_url?.[locale] || doc.external_url?.[locale] || bundledRd?.landing_urls?.[locale] || null, {
|
|
257
870
|
source: 'sensecraft_agent',
|
|
258
871
|
medium: 'solution_consult',
|
|
259
872
|
campaign: docId,
|
|
@@ -265,6 +878,15 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
265
878
|
campaign: docId,
|
|
266
879
|
}) : '',
|
|
267
880
|
repo_url: repoUrl,
|
|
881
|
+
title_locale: titleText.locale,
|
|
882
|
+
summary_locale: summaryText.locale,
|
|
883
|
+
content_locale: summaryText.locale || titleText.locale,
|
|
884
|
+
fallback: (summaryText.locale || titleText.locale) !== null && (summaryText.locale || titleText.locale) !== locale,
|
|
885
|
+
...(archPreview ? { architecture_preview: archPreview } : {}),
|
|
886
|
+
scenario_ids: bundledRd?.scenario_ids || [],
|
|
887
|
+
technology_ids: bundledRd?.technology_ids || [],
|
|
888
|
+
details_available: Boolean(bundledRd),
|
|
889
|
+
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
890
|
diagnostic_playbook: pb,
|
|
269
891
|
source: 'typesense',
|
|
270
892
|
});
|
|
@@ -281,27 +903,11 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
281
903
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
282
904
|
if (s[`visibility_${locKey}`] === 'staging' || s[`visibility_${locKey}`] === 'draft') continue;
|
|
283
905
|
|
|
284
|
-
const
|
|
906
|
+
const localizedFields = Object.values(s.locale_content || {}).flatMap((content) => [content?.title, content?.summary]);
|
|
907
|
+
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
908
|
const score = tokens.reduce((acc, t) => (haystack.includes(t) ? acc + 1 : acc), 0);
|
|
286
909
|
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
|
-
});
|
|
910
|
+
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
911
|
}
|
|
306
912
|
}
|
|
307
913
|
}
|
|
@@ -311,30 +917,11 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
311
917
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
312
918
|
if (rd[`visibility_${locKey}`] === 'staging' || rd[`visibility_${locKey}`] === 'draft') continue;
|
|
313
919
|
|
|
314
|
-
const
|
|
920
|
+
const localizedFields = Object.values(rd.locale_content || {}).flatMap((content) => [content?.title, content?.summary]);
|
|
921
|
+
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
922
|
const score = tokens.reduce((acc, t) => (haystack.includes(t) ? acc + 1 : acc), 0);
|
|
316
923
|
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
|
-
});
|
|
924
|
+
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
925
|
}
|
|
339
926
|
}
|
|
340
927
|
}
|
|
@@ -354,8 +941,11 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
354
941
|
if (mergedRecords.length > 0) {
|
|
355
942
|
return createCatalogDto({
|
|
356
943
|
source: (onlineScenarios.some(r => r.source === 'typesense') || onlineRefDesigns.some(r => r.source === 'typesense')) ? 'typesense' : 'bundled_manifest',
|
|
357
|
-
freshness: '
|
|
358
|
-
|
|
944
|
+
freshness: 'UNKNOWN',
|
|
945
|
+
datasetVersion: core.dataset_version,
|
|
946
|
+
scope: 'online_partial_with_bundled_fallback',
|
|
947
|
+
warnings: ['Online search results are partial and do not replace the bundled catalog; use hub get for verified section content.'],
|
|
948
|
+
records: mergedRecords.map(compactSearchRecord),
|
|
359
949
|
});
|
|
360
950
|
}
|
|
361
951
|
} catch {
|
|
@@ -375,32 +965,26 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
375
965
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
376
966
|
if (s[`visibility_${locKey}`] === 'staging' || s[`visibility_${locKey}`] === 'draft') continue;
|
|
377
967
|
|
|
968
|
+
const localizedFields = Object.values(s.locale_content || {}).flatMap((content) => [content?.title, content?.summary]);
|
|
378
969
|
const haystack = [
|
|
379
970
|
s.id,
|
|
380
971
|
s.title?.zh,
|
|
381
972
|
s.title?.en,
|
|
973
|
+
s.title?.ja,
|
|
382
974
|
s.subtitle?.zh,
|
|
383
975
|
s.subtitle?.en,
|
|
976
|
+
s.subtitle?.ja,
|
|
384
977
|
s.summary,
|
|
978
|
+
...localizedFields,
|
|
385
979
|
...(s.tags || []),
|
|
386
980
|
].join(' ').toLowerCase();
|
|
387
981
|
|
|
388
982
|
const score = tokens.reduce((acc, token) => (haystack.includes(token) ? acc + 1 : acc), 0);
|
|
389
983
|
if (tokens.length === 0 || score > 0) {
|
|
390
984
|
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 || '',
|
|
985
|
+
...bundledSearchProjection({ ...s, summary: s.subtitle || s.summary }, recordType(s, 'scenario'), locale),
|
|
395
986
|
tags: s.tags || [],
|
|
396
987
|
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
988
|
repo_url: s.repo_url || '',
|
|
405
989
|
diagnostic_playbook: s.diagnostic_playbook || null,
|
|
406
990
|
score,
|
|
@@ -413,29 +997,24 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
413
997
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
414
998
|
if (rd[`visibility_${locKey}`] === 'staging' || rd[`visibility_${locKey}`] === 'draft') continue;
|
|
415
999
|
|
|
1000
|
+
const localizedFields = Object.values(rd.locale_content || {}).flatMap((content) => [content?.title, content?.summary]);
|
|
416
1001
|
const haystack = [
|
|
417
1002
|
rd.id,
|
|
418
1003
|
rd.title?.zh,
|
|
419
1004
|
rd.title?.en,
|
|
1005
|
+
rd.title?.ja,
|
|
420
1006
|
rd.summary?.zh,
|
|
421
1007
|
rd.summary?.en,
|
|
1008
|
+
rd.summary?.ja,
|
|
1009
|
+
...localizedFields,
|
|
422
1010
|
...(rd.presets || []),
|
|
423
1011
|
].join(' ').toLowerCase();
|
|
424
1012
|
|
|
425
1013
|
const score = tokens.reduce((acc, token) => (haystack.includes(token) ? acc + 1 : acc), 0);
|
|
426
1014
|
if (tokens.length === 0 || score > 0) {
|
|
427
1015
|
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 || '',
|
|
1016
|
+
...bundledSearchProjection(rd, 'reference_design', locale),
|
|
432
1017
|
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
1018
|
wiki_url: rd.wiki_url ? appendUtm(rd.wiki_url, {
|
|
440
1019
|
source: 'sensecraft_agent',
|
|
441
1020
|
medium: 'tech_wiki',
|
|
@@ -465,11 +1044,15 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
465
1044
|
|
|
466
1045
|
return createCatalogDto({
|
|
467
1046
|
source: 'bundled_manifest',
|
|
468
|
-
freshness: '
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
1047
|
+
freshness: 'UNKNOWN',
|
|
1048
|
+
datasetVersion: core.dataset_version,
|
|
1049
|
+
scope: 'bundled_catalog_snapshot',
|
|
1050
|
+
records: combined.map(compactSearchRecord),
|
|
1051
|
+
warnings: process.env.SENSECRAFT_OFFLINE_ONLY
|
|
1052
|
+
? ['Offline-only mode: displaying the verified bundled catalog; no online request was attempted.']
|
|
1053
|
+
: ((keySolution || keyRefDesign)
|
|
1054
|
+
? ['Online Typesense request timed out or returned no hits. Displaying verified local catalog.']
|
|
1055
|
+
: ['No Typesense API key configured or baked. Displaying verified local catalog.']),
|
|
473
1056
|
});
|
|
474
1057
|
}
|
|
475
1058
|
|
|
@@ -524,11 +1107,116 @@ export function queryProductFamily(familyId, { locale = 'zh' } = {}) {
|
|
|
524
1107
|
*
|
|
525
1108
|
* @param {string} familyId
|
|
526
1109
|
* @param {Object} [options]
|
|
1110
|
+
/**
|
|
1111
|
+
* Bazaar bulk-add-cart supports simple numeric SKUs.
|
|
1112
|
+
*
|
|
1113
|
+
* @param {string|null|undefined} sku
|
|
1114
|
+
* @returns {boolean}
|
|
1115
|
+
*/
|
|
1116
|
+
export function isCartableSku(sku) {
|
|
1117
|
+
if (!sku) return false;
|
|
1118
|
+
return /^\d+$/.test(String(sku).trim());
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Storefront domain per site locale (ja -> jp.seeedstudio.com, else www.seeedstudio.com).
|
|
1123
|
+
*
|
|
1124
|
+
* @param {string} [locale='en']
|
|
1125
|
+
* @returns {string}
|
|
1126
|
+
*/
|
|
1127
|
+
export function storeBaseUrlForLocale(locale) {
|
|
1128
|
+
return locale === 'ja'
|
|
1129
|
+
? 'https://jp.seeedstudio.com'
|
|
1130
|
+
: 'https://www.seeedstudio.com';
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Chinese locale does not check out on global store.
|
|
1135
|
+
*
|
|
1136
|
+
* @param {string} [locale]
|
|
1137
|
+
* @returns {boolean}
|
|
1138
|
+
*/
|
|
1139
|
+
export function localeSupportsCart(locale) {
|
|
1140
|
+
return locale !== 'zh-hans' && locale !== 'zh';
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Builds Seeed Bazaar bulk-add-cart deep link with UTM attribution.
|
|
1145
|
+
*
|
|
1146
|
+
* @param {Array<{sku: string, qty: number}>} items
|
|
1147
|
+
* @param {Object} [options]
|
|
1148
|
+
* @param {string} [options.solutionSlug='solution']
|
|
1149
|
+
* @param {string} [options.locale='en']
|
|
1150
|
+
* @param {string} [options.requestId]
|
|
1151
|
+
* @returns {string|null}
|
|
1152
|
+
*/
|
|
1153
|
+
export function buildBulkAddCartUrl(items, { solutionSlug = 'solution', locale = 'en', requestId = null } = {}) {
|
|
1154
|
+
const validItems = (items || [])
|
|
1155
|
+
.filter((it) => it && isCartableSku(it.sku) && Number.isInteger(it.qty) && it.qty > 0)
|
|
1156
|
+
.map((it) => `${it.sku}:${it.qty}`);
|
|
1157
|
+
|
|
1158
|
+
if (validItems.length === 0) return null;
|
|
1159
|
+
if (!localeSupportsCart(locale)) return null;
|
|
1160
|
+
|
|
1161
|
+
const reqId = requestId || `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1162
|
+
const medium = encodeURIComponent(`Seeed::Solution::${solutionSlug}::sample-purchase`);
|
|
1163
|
+
const baseUrl = storeBaseUrlForLocale(locale);
|
|
1164
|
+
const rawUrl = `${baseUrl}/bulk-add-cart?items=${validItems.join(',')}&requestId=${encodeURIComponent(reqId)}&c_event_medium=${medium}`;
|
|
1165
|
+
|
|
1166
|
+
return appendUtm(rawUrl, {
|
|
1167
|
+
source: 'sensecraft_agent',
|
|
1168
|
+
medium: 'solution_bom',
|
|
1169
|
+
campaign: solutionSlug,
|
|
1170
|
+
content: 'sample_purchase',
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Evaluates whether an accessory with applies_when condition matches the selected host SKU.
|
|
1176
|
+
*
|
|
1177
|
+
* @param {Object} acc - Accessory definition
|
|
1178
|
+
* @param {string} hostSku - Selected host SKU
|
|
1179
|
+
* @param {Object} family - Product family object
|
|
1180
|
+
* @returns {boolean}
|
|
1181
|
+
*/
|
|
1182
|
+
export function isAccessoryApplicable(acc, hostSku, family) {
|
|
1183
|
+
if (!acc || !acc.applies_when) {
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
const { attr, value } = acc.applies_when;
|
|
1187
|
+
if (!attr) return true;
|
|
1188
|
+
|
|
1189
|
+
const skuObj = (family?.skus || []).find((s) => s.sku === hostSku);
|
|
1190
|
+
const actualValue = skuObj?.attrs?.[attr];
|
|
1191
|
+
|
|
1192
|
+
if (actualValue === undefined || actualValue === null) {
|
|
1193
|
+
const fallbackValue = family?.[attr] ?? family?.physical_spec?.[attr];
|
|
1194
|
+
if (fallbackValue !== undefined && fallbackValue !== null) {
|
|
1195
|
+
return Array.isArray(value) ? value.includes(fallbackValue) : value === fallbackValue;
|
|
1196
|
+
}
|
|
1197
|
+
if (skuObj && skuObj.attrs) {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (Array.isArray(value)) {
|
|
1204
|
+
return value.includes(actualValue);
|
|
1205
|
+
}
|
|
1206
|
+
return value === actualValue;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Queries accessories for a given product family and evaluates compatibility.
|
|
1211
|
+
*
|
|
1212
|
+
* @param {string} familyId - Product family ID
|
|
1213
|
+
* @param {Object} [options]
|
|
527
1214
|
* @param {string} [options.sku] - Specific host SKU
|
|
528
1215
|
* @param {string} [options.locale='zh']
|
|
1216
|
+
* @param {boolean} [options.compact=false] - Return token-efficient compact payload
|
|
529
1217
|
* @returns {Object} Catalog DTO with structured, verified accessories
|
|
530
1218
|
*/
|
|
531
|
-
export function queryAccessories(familyId, { sku = null, locale = 'zh' } = {}) {
|
|
1219
|
+
export function queryAccessories(familyId, { sku = null, locale = 'zh', compact = false, category = null } = {}) {
|
|
532
1220
|
const core = loadKnowledgeCore();
|
|
533
1221
|
const family = core.product_families?.[familyId];
|
|
534
1222
|
|
|
@@ -560,11 +1248,13 @@ export function queryAccessories(familyId, { sku = null, locale = 'zh' } = {}) {
|
|
|
560
1248
|
qty: acc.qty || 1,
|
|
561
1249
|
reason: acc.reason || 'Required for standard operation',
|
|
562
1250
|
applies_when: acc.applies_when || null,
|
|
563
|
-
compatibility:
|
|
564
|
-
is_compatible: comp.isCompatible,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1251
|
+
compatibility: compact
|
|
1252
|
+
? { is_compatible: comp.isCompatible, status: comp.status }
|
|
1253
|
+
: {
|
|
1254
|
+
is_compatible: comp.isCompatible,
|
|
1255
|
+
status: comp.status,
|
|
1256
|
+
details: comp.dimensions,
|
|
1257
|
+
},
|
|
568
1258
|
};
|
|
569
1259
|
});
|
|
570
1260
|
|
|
@@ -583,25 +1273,73 @@ export function queryAccessories(familyId, { sku = null, locale = 'zh' } = {}) {
|
|
|
583
1273
|
content: `optional_${acc.id}`,
|
|
584
1274
|
}),
|
|
585
1275
|
type: acc.type || 'peripheral',
|
|
586
|
-
compatibility:
|
|
587
|
-
is_compatible: comp.isCompatible,
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1276
|
+
compatibility: compact
|
|
1277
|
+
? { is_compatible: comp.isCompatible, status: comp.status }
|
|
1278
|
+
: {
|
|
1279
|
+
is_compatible: comp.isCompatible,
|
|
1280
|
+
status: comp.status,
|
|
1281
|
+
details: comp.dimensions,
|
|
1282
|
+
},
|
|
591
1283
|
};
|
|
592
1284
|
});
|
|
593
1285
|
|
|
1286
|
+
let filteredRequired = required;
|
|
1287
|
+
let filteredOptional = optional;
|
|
1288
|
+
|
|
1289
|
+
if (category) {
|
|
1290
|
+
const catTokens = String(category).toLowerCase().split(',').map((s) => s.trim()).filter(Boolean);
|
|
1291
|
+
if (catTokens.length > 0) {
|
|
1292
|
+
filteredRequired = required.filter((acc) => {
|
|
1293
|
+
if (catTokens.includes('required')) return true;
|
|
1294
|
+
if (catTokens.includes('optional')) return false;
|
|
1295
|
+
const txt = [acc.id, acc.name, acc.sku, acc.category, acc.reason].join(' ').toLowerCase();
|
|
1296
|
+
return catTokens.some((tok) => txt.includes(tok));
|
|
1297
|
+
});
|
|
1298
|
+
filteredOptional = optional.filter((acc) => {
|
|
1299
|
+
if (catTokens.includes('optional')) return true;
|
|
1300
|
+
if (catTokens.includes('required')) return false;
|
|
1301
|
+
const txt = [acc.id, acc.name, acc.sku, acc.category, acc.type].join(' ').toLowerCase();
|
|
1302
|
+
return catTokens.some((tok) => txt.includes(tok));
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const hostSku = sku || family.default_sku;
|
|
1308
|
+
const cartItems = [];
|
|
1309
|
+
if (isCartableSku(hostSku)) {
|
|
1310
|
+
cartItems.push({ sku: hostSku, qty: 1 });
|
|
1311
|
+
}
|
|
1312
|
+
for (const req of filteredRequired) {
|
|
1313
|
+
const isApplicable = isAccessoryApplicable(req, hostSku, family);
|
|
1314
|
+
req.applicable = isApplicable;
|
|
1315
|
+
if (isApplicable && isCartableSku(req.sku)) {
|
|
1316
|
+
cartItems.push({ sku: req.sku, qty: req.qty || 1 });
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const bundleCartUrl = buildBulkAddCartUrl(cartItems, {
|
|
1321
|
+
solutionSlug: family.id,
|
|
1322
|
+
locale,
|
|
1323
|
+
});
|
|
1324
|
+
|
|
594
1325
|
return createCatalogDto({
|
|
595
1326
|
source: 'bundled_manifest',
|
|
596
1327
|
freshness: 'FRESH',
|
|
597
1328
|
records: [{
|
|
598
1329
|
family_id: family.id,
|
|
599
1330
|
host_title: family.title?.[locale] || family.title?.zh || family.title?.en,
|
|
600
|
-
host_sku:
|
|
1331
|
+
host_sku: hostSku,
|
|
601
1332
|
physical_spec: hostPhysicalSpec,
|
|
1333
|
+
bundle_cart_url: bundleCartUrl,
|
|
1334
|
+
...(bundleCartUrl ? {} : {
|
|
1335
|
+
domestic_channel_note: locale === 'zh'
|
|
1336
|
+
? '国内采购建议通过 Seeed 官方直营店铺或授权分销商采购(非国际站美金结算)'
|
|
1337
|
+
: null,
|
|
1338
|
+
}),
|
|
1339
|
+
...(category ? { filtered_by_category: category } : {}),
|
|
602
1340
|
accessories: {
|
|
603
|
-
required,
|
|
604
|
-
optional,
|
|
1341
|
+
required: filteredRequired,
|
|
1342
|
+
optional: filteredOptional,
|
|
605
1343
|
},
|
|
606
1344
|
}],
|
|
607
1345
|
});
|