@seeed-studio/solution-cli 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -12
- package/bin/cli.js +115 -8
- package/catalog/knowledge-core.json +17440 -574
- package/docs/cli-guide.md +34 -11
- package/lib/agent.js +77 -16
- package/lib/installer.js +14 -0
- package/lib/knowledge.js +308 -9
- package/package.json +4 -3
- package/skills/solution-cli/SKILL.md +14 -4
package/docs/cli-guide.md
CHANGED
|
@@ -28,24 +28,26 @@ Agent 的默认工作流是“先咨询,再选型,再检查,最后在用
|
|
|
28
28
|
|
|
29
29
|
## 2. 安装与 Agent 技能挂载
|
|
30
30
|
|
|
31
|
-
### 2.1
|
|
31
|
+
### 2.1 按需精准安装(推荐:npm 全局安装并挂载,避免 PATH 缺失报错)
|
|
32
32
|
|
|
33
33
|
你可以直接复制下表中对应的专属命令发给对应的 AI 助手,或在终端执行:
|
|
34
34
|
|
|
35
35
|
| 目标 AI 宿主平台 | 专属安装命令(直接复制发给 AI 或在终端运行) | 技能默认配置路径 |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| **豆包工作 (Doubao Work)** | `
|
|
38
|
-
| **
|
|
39
|
-
| **
|
|
40
|
-
| **
|
|
41
|
-
| **
|
|
42
|
-
| **CodeBuddy** | `
|
|
43
|
-
|
|
|
44
|
-
|
|
|
37
|
+
| **豆包工作 (Doubao Work)** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent doubao` | `~/Doubao/skills` (及 `~/.agents/skills`) |
|
|
38
|
+
| **Claude Code** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent claude` | `~/.claude/skills` |
|
|
39
|
+
| **OpenAI Codex** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent codex` | `~/.agents/skills` 或 `~/.codex/skills` |
|
|
40
|
+
| **Google Antigravity** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent antigravity` | `~/.gemini/config/skills` |
|
|
41
|
+
| **WorkBuddy** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent workbuddy` | `~/.workbuddy/skills` |
|
|
42
|
+
| **CodeBuddy** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent codebuddy` | `~/.codebuddy/skills` |
|
|
43
|
+
| **Hermes Agent** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent hermes` | `$HERMES_HOME/skills` (默认 `~/.hermes/skills`) |
|
|
44
|
+
| **全部 Agent (全量覆盖)** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent all` | 上述全部已检测目录 |
|
|
45
|
+
| **自定义目录 (如 Cursor)** | `npm install -g @seeed-studio/solution-cli && solution-cli install --skills-dir <路径>` | `<自定义路径>/solution-cli` |
|
|
45
46
|
|
|
46
47
|
> [!TIP]
|
|
47
|
-
> -
|
|
48
|
-
> -
|
|
48
|
+
> - **为什么推荐 npm 全局安装?** 将 `solution-cli` 加入全局 PATH,AI Agent 在后续对话中调用方案/硬件查询时直接本地毫秒级响应,避免因 PATH 缺失触发 `command not found`。
|
|
49
|
+
> - **发给 AI 助手**:例如在豆包工作中直接发送:*“请帮我运行:`npm install -g @seeed-studio/solution-cli && solution-cli install --agent doubao`”*。
|
|
50
|
+
> - **免全局安装(npx 备选)**:如无全局 npm 写入权限,可使用 npx:`npx -y @seeed-studio/solution-cli install --agent <id>`。
|
|
49
51
|
|
|
50
52
|
### 2.2 本地源码部署与定制开发 (Local Deployment)
|
|
51
53
|
|
|
@@ -59,6 +61,27 @@ node bin/cli.js install --agent <doubao|claude|codex|antigravity|codebuddy>
|
|
|
59
61
|
node bin/cli.js install --skills-dir ~/.cursor/skills
|
|
60
62
|
```
|
|
61
63
|
|
|
64
|
+
省略 `--agent` 时,终端交互运行会进入选择器;非交互运行会安装到已检测的宿主,未检测到宿主时会选择全部宿主。建议始终明确指定当前 Agent。重复执行会复用相同或更新的签名 Engine、升级旧版本,并保留用户修改的 Skill。`--force` 会覆盖已有 Skill 修改,只在用户明确要求时使用。
|
|
65
|
+
|
|
66
|
+
安装完成后刷新宿主的 Skill 列表或完全退出并重新打开 Agent 应用,确认宿主的实际 Skill 列表中已出现 `solution-cli`,再新建会话直接描述需求。若宿主未自动发现技能,发送:“请使用 solution-cli Skill,先查询方案,再告诉我缺哪些部署信息。”安装器只写入被选中的宿主。
|
|
67
|
+
|
|
68
|
+
### 2.3 全局安装与 Profile
|
|
69
|
+
|
|
70
|
+
需要长期使用 `solution-cli` 命令时可以全局安装:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install -g --registry=https://registry.npmjs.org/ @seeed-studio/solution-cli@latest
|
|
74
|
+
solution-cli status
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
不想修改全局 npm 环境时,使用 `npx --package` 临时调用:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx -y --registry=https://registry.npmjs.org/ --package=@seeed-studio/solution-cli@latest solution-cli hub search "农业" --json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Hermes 的已配置 profile 可通过 `--agent hermes:<profile>` 选择(安装、更新、卸载通用):具名 profile 使用 `<root>/profiles/<profile>/skills/solution-cli`,`hermes:default` 使用 `<root>/skills/solution-cli`;`<root>` 是 Hermes 安装根目录。Profile ID 为 1–64 位小写字母、数字、`_` 或 `-`,以字母或数字开头。安装器仅复制 Skill,不创建或切换 profile。
|
|
84
|
+
|
|
62
85
|
## 3. 核心功能、使用方法与具体产出
|
|
63
86
|
|
|
64
87
|
安装后可直接在命令行运行 `solution-cli` 与 `sensecraft-engine`;若未执行安装,也可随时通过 `npx -y @seeed-studio/solution-cli <command>` 免安装即用。
|
package/lib/agent.js
CHANGED
|
@@ -5,6 +5,53 @@ import readline from 'node:readline';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { loadLedger } from './ledger.js';
|
|
7
7
|
|
|
8
|
+
// Match Hermes' native data home and get_default_hermes_root() semantics.
|
|
9
|
+
function getHermesPaths() {
|
|
10
|
+
const home = os.homedir();
|
|
11
|
+
const suffix = process.env.HERMES_DATA_DIR_SUFFIX || '';
|
|
12
|
+
const nativeHome = process.platform === 'win32'
|
|
13
|
+
? path.join(process.env.LOCALAPPDATA?.trim() || path.join(home, 'AppData', 'Local'), `hermes${suffix}`)
|
|
14
|
+
: path.join(home, `.hermes${suffix}`);
|
|
15
|
+
const expand = value => value.replace(/\$(\w+)|\$\{([^}]+)\}/g,
|
|
16
|
+
(match, bare, braced) => process.env[bare || braced] ?? match)
|
|
17
|
+
.replace(/^~(?=$|[\\/])/, () => home);
|
|
18
|
+
const activeHome = path.resolve(expand(process.env.HERMES_HOME?.trim() || nativeHome));
|
|
19
|
+
const real = value => fs.existsSync(value) ? fs.realpathSync(value) : path.resolve(value);
|
|
20
|
+
const relative = path.relative(real(nativeHome), real(activeHome));
|
|
21
|
+
const inNativeHome = relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
22
|
+
const root = inNativeHome ? nativeHome
|
|
23
|
+
: path.basename(path.dirname(activeHome)) === 'profiles' ? path.dirname(path.dirname(activeHome)) : activeHome;
|
|
24
|
+
return { activeHome, root };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hermesTarget(id = 'hermes') {
|
|
28
|
+
const { activeHome, root } = getHermesPaths();
|
|
29
|
+
let targetHome = activeHome;
|
|
30
|
+
if (id !== 'hermes') {
|
|
31
|
+
const profile = id.slice('hermes:'.length);
|
|
32
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(profile)) {
|
|
33
|
+
throw new Error(`Invalid Hermes profile: "${profile}". Use 1-64 lowercase letters, numbers, '-' or '_', starting with a letter or number.`);
|
|
34
|
+
}
|
|
35
|
+
targetHome = profile === 'default' ? root : path.join(root, 'profiles', profile);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
name: id === 'hermes' ? 'Hermes Agent' : `Hermes Agent (${id.slice(7)})`,
|
|
40
|
+
homeDirs: [targetHome],
|
|
41
|
+
getSkillsDir: () => path.join(targetHome, 'skills'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scanAgent(agent) {
|
|
46
|
+
const skillsDir = agent.getSkillsDir();
|
|
47
|
+
return {
|
|
48
|
+
...agent,
|
|
49
|
+
detected: agent.homeDirs.some(dir => fs.existsSync(dir)) || fs.existsSync(skillsDir),
|
|
50
|
+
installed: fs.existsSync(path.join(skillsDir, 'solution-cli', 'SKILL.md')),
|
|
51
|
+
skillsDir,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
8
55
|
export function getKnownAgents() {
|
|
9
56
|
const home = os.homedir();
|
|
10
57
|
|
|
@@ -12,8 +59,19 @@ export function getKnownAgents() {
|
|
|
12
59
|
{
|
|
13
60
|
id: 'doubao',
|
|
14
61
|
name: '豆包工作 (Doubao Work)',
|
|
15
|
-
homeDirs: [path.join(home, '.doubao')],
|
|
16
|
-
getSkillsDir: () =>
|
|
62
|
+
homeDirs: [path.join(home, 'Doubao'), path.join(home, '.doubao')],
|
|
63
|
+
getSkillsDir: () => {
|
|
64
|
+
const pDoubao = path.join(home, 'Doubao', 'skills');
|
|
65
|
+
const pAgents = path.join(home, '.agents', 'skills');
|
|
66
|
+
const pLegacy = path.join(home, '.doubao', 'skills');
|
|
67
|
+
if (fs.existsSync(path.join(pDoubao, 'solution-cli', 'SKILL.md'))) return pDoubao;
|
|
68
|
+
if (fs.existsSync(path.join(pAgents, 'solution-cli', 'SKILL.md'))) return pAgents;
|
|
69
|
+
if (fs.existsSync(path.join(pLegacy, 'solution-cli', 'SKILL.md'))) return pLegacy;
|
|
70
|
+
if (fs.existsSync(path.join(home, 'Doubao'))) return pDoubao;
|
|
71
|
+
if (fs.existsSync(path.join(home, '.agents'))) return pAgents;
|
|
72
|
+
if (fs.existsSync(path.join(home, '.doubao'))) return pLegacy;
|
|
73
|
+
return pDoubao;
|
|
74
|
+
}
|
|
17
75
|
},
|
|
18
76
|
{
|
|
19
77
|
id: 'workbuddy',
|
|
@@ -68,7 +126,8 @@ export function getKnownAgents() {
|
|
|
68
126
|
name: 'Google Antigravity (AGY)',
|
|
69
127
|
homeDirs: [path.join(home, '.gemini', 'config'), path.join(home, '.gemini', 'antigravity-cli')],
|
|
70
128
|
getSkillsDir: () => path.join(home, '.gemini', 'config', 'skills')
|
|
71
|
-
}
|
|
129
|
+
},
|
|
130
|
+
hermesTarget()
|
|
72
131
|
];
|
|
73
132
|
}
|
|
74
133
|
|
|
@@ -78,6 +137,10 @@ export function scanLocalAgents() {
|
|
|
78
137
|
try {
|
|
79
138
|
const ledger = loadLedger();
|
|
80
139
|
for (const [targetId, inst] of Object.entries(ledger?.installations || {})) {
|
|
140
|
+
if (targetId.startsWith('hermes:') && inst && !agents.some(a => a.id === targetId)) {
|
|
141
|
+
const target = hermesTarget(targetId);
|
|
142
|
+
if (fs.existsSync(path.join(target.getSkillsDir(), 'solution-cli', 'SKILL.md'))) agents.push(target);
|
|
143
|
+
}
|
|
81
144
|
if (targetId.startsWith('custom_') && inst) {
|
|
82
145
|
const skillsDir = inst.skills_dir || (inst.target_dir ? path.dirname(inst.target_dir) : (inst.targetDir ? path.dirname(inst.targetDir) : null));
|
|
83
146
|
if (skillsDir && fs.existsSync(skillsDir) && !agents.some(a => a.id === targetId)) {
|
|
@@ -92,17 +155,7 @@ export function scanLocalAgents() {
|
|
|
92
155
|
}
|
|
93
156
|
} catch {}
|
|
94
157
|
|
|
95
|
-
return agents.map(
|
|
96
|
-
const skillsDir = a.getSkillsDir();
|
|
97
|
-
const detected = a.homeDirs.some(dir => fs.existsSync(dir)) || fs.existsSync(skillsDir);
|
|
98
|
-
const installed = fs.existsSync(path.join(skillsDir, 'solution-cli', 'SKILL.md'));
|
|
99
|
-
return {
|
|
100
|
-
...a,
|
|
101
|
-
detected,
|
|
102
|
-
installed,
|
|
103
|
-
skillsDir
|
|
104
|
-
};
|
|
105
|
-
});
|
|
158
|
+
return agents.map(scanAgent);
|
|
106
159
|
}
|
|
107
160
|
|
|
108
161
|
export function formatAgentList(scannedAgents) {
|
|
@@ -147,8 +200,16 @@ export async function promptAgentSelection(scannedAgents, cliArg, autoYes = fals
|
|
|
147
200
|
if (cliArg.toLowerCase() === 'all') {
|
|
148
201
|
return scannedAgents;
|
|
149
202
|
}
|
|
150
|
-
const requested = cliArg.split(',').map(s => s.trim()
|
|
151
|
-
|
|
203
|
+
const requested = cliArg.split(',').map(s => s.trim());
|
|
204
|
+
// Validate profile IDs before normalizing other hosts or accepting a partial match.
|
|
205
|
+
for (const id of requested) {
|
|
206
|
+
if (id.toLowerCase().startsWith('hermes:')) {
|
|
207
|
+
const target = scanAgent(hermesTarget(`hermes:${id.slice(7)}`));
|
|
208
|
+
if (!scannedAgents.some(a => a.id === target.id)) scannedAgents.push(target);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const normalized = requested.map(id => id.toLowerCase());
|
|
212
|
+
const matched = scannedAgents.filter(a => normalized.includes(a.id) || a.aliases?.some(alias => normalized.includes(alias)));
|
|
152
213
|
if (matched.length === 0) {
|
|
153
214
|
throw new Error(`No matching agents for: "${cliArg}". Available: ${scannedAgents.map(a => a.id).join(', ')}`);
|
|
154
215
|
}
|
package/lib/installer.js
CHANGED
|
@@ -241,6 +241,20 @@ export async function runInstall(options = {}) {
|
|
|
241
241
|
recordInstallation(a.id, a.skillsDir, destDir, engineVersion, filesWritten);
|
|
242
242
|
logger(` ✔ Skill successfully installed to: ${destDir}`);
|
|
243
243
|
installedAgents.push({ id: a.id, name: a.name, path: destDir });
|
|
244
|
+
|
|
245
|
+
// For doubao agent, ensure secondary scanning directory ~/.agents/skills has symlink if possible
|
|
246
|
+
if (a.id === 'doubao') {
|
|
247
|
+
try {
|
|
248
|
+
const secondarySkills = path.join(os.homedir(), '.agents', 'skills');
|
|
249
|
+
const secondaryDest = path.join(secondarySkills, SKILL_DIR_NAME);
|
|
250
|
+
if (path.resolve(a.skillsDir) !== path.resolve(secondarySkills)) {
|
|
251
|
+
if (fs.existsSync(secondarySkills) && !fs.existsSync(secondaryDest)) {
|
|
252
|
+
fs.symlinkSync(destDir, secondaryDest, 'junction');
|
|
253
|
+
logger(` ✔ Created compatibility link for Doubao: ${secondaryDest} -> ${destDir}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
244
258
|
}
|
|
245
259
|
});
|
|
246
260
|
|
package/lib/knowledge.js
CHANGED
|
@@ -711,6 +711,35 @@ export function getHubRecord(id, { type = null, locale = 'zh', section = null, s
|
|
|
711
711
|
return row;
|
|
712
712
|
}
|
|
713
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
|
+
|
|
714
743
|
/**
|
|
715
744
|
* Searches Seeed Solutions Hub scenarios and reference designs.
|
|
716
745
|
*
|
|
@@ -725,6 +754,7 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
725
754
|
locale = normalizedLocale;
|
|
726
755
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new Error('Limit must be an integer from 1 to 200.');
|
|
727
756
|
const qLower = (query || '').toLowerCase().trim();
|
|
757
|
+
const tokens = qLower.split(/\s+/).filter(Boolean);
|
|
728
758
|
const clientConfig = loadClientConfig();
|
|
729
759
|
const host = process.env.TYPESENSE_HOST || clientConfig?.typesense?.host || 'https://rel-search.seeedstudio.com';
|
|
730
760
|
const keySolution = process.env.TYPESENSE_SEARCH_KEY_SOLUTION || process.env.TYPESENSE_SEARCH_KEY || clientConfig?.typesense?.search_key_solution;
|
|
@@ -896,7 +926,6 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
896
926
|
|
|
897
927
|
// If one collection returned results but the other was unavailable/failed/empty, supplement from bundled catalog
|
|
898
928
|
if ((onlineScenarios.length > 0 && onlineRefDesigns.length === 0) || (onlineRefDesigns.length > 0 && onlineScenarios.length === 0)) {
|
|
899
|
-
const tokens = qLower.split(/\s+/).filter(Boolean);
|
|
900
929
|
if (onlineScenarios.length === 0) {
|
|
901
930
|
for (const s of core.scenarios || []) {
|
|
902
931
|
if (s.published === false || s.visibility === 'staging' || s.visibility === 'draft') continue;
|
|
@@ -939,12 +968,17 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
939
968
|
}
|
|
940
969
|
|
|
941
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
|
+
}
|
|
942
976
|
return createCatalogDto({
|
|
943
977
|
source: (onlineScenarios.some(r => r.source === 'typesense') || onlineRefDesigns.some(r => r.source === 'typesense')) ? 'typesense' : 'bundled_manifest',
|
|
944
978
|
freshness: 'UNKNOWN',
|
|
945
979
|
datasetVersion: core.dataset_version,
|
|
946
980
|
scope: 'online_partial_with_bundled_fallback',
|
|
947
|
-
warnings:
|
|
981
|
+
warnings: onlineWarnings,
|
|
948
982
|
records: mergedRecords.map(compactSearchRecord),
|
|
949
983
|
});
|
|
950
984
|
}
|
|
@@ -958,8 +992,6 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
958
992
|
const matchedScenarios = [];
|
|
959
993
|
const matchedRefDesigns = [];
|
|
960
994
|
|
|
961
|
-
const tokens = qLower.split(/\s+/).filter(Boolean);
|
|
962
|
-
|
|
963
995
|
for (const s of core.scenarios || []) {
|
|
964
996
|
if (s.published === false || s.visibility === 'staging' || s.visibility === 'draft') continue;
|
|
965
997
|
const locKey = locale === 'zh' ? 'zh_hans' : locale;
|
|
@@ -1042,17 +1074,25 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
|
|
|
1042
1074
|
}
|
|
1043
1075
|
}
|
|
1044
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
|
+
|
|
1045
1089
|
return createCatalogDto({
|
|
1046
1090
|
source: 'bundled_manifest',
|
|
1047
1091
|
freshness: 'UNKNOWN',
|
|
1048
1092
|
datasetVersion: core.dataset_version,
|
|
1049
1093
|
scope: 'bundled_catalog_snapshot',
|
|
1050
1094
|
records: combined.map(compactSearchRecord),
|
|
1051
|
-
warnings
|
|
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.']),
|
|
1095
|
+
warnings,
|
|
1056
1096
|
});
|
|
1057
1097
|
}
|
|
1058
1098
|
|
|
@@ -1083,8 +1123,12 @@ export function queryProductFamily(familyId, { locale = 'zh' } = {}) {
|
|
|
1083
1123
|
records: [{
|
|
1084
1124
|
family_id: family.id,
|
|
1085
1125
|
title: family.title?.[locale] || family.title?.zh || family.title?.en || family.id,
|
|
1126
|
+
summary: family.summary || null,
|
|
1086
1127
|
default_sku: family.default_sku,
|
|
1087
1128
|
regions: family.regions || [],
|
|
1129
|
+
interfaces: family.interfaces || [],
|
|
1130
|
+
protocols: family.protocols || [],
|
|
1131
|
+
supported_scenarios: family.supported_scenarios || [],
|
|
1088
1132
|
skus_count: (family.skus || []).length,
|
|
1089
1133
|
skus: (family.skus || []).slice(0, 10).map((s) => ({
|
|
1090
1134
|
...s,
|
|
@@ -1102,6 +1146,261 @@ export function queryProductFamily(familyId, { locale = 'zh' } = {}) {
|
|
|
1102
1146
|
});
|
|
1103
1147
|
}
|
|
1104
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
|
+
|
|
1105
1404
|
/**
|
|
1106
1405
|
* Queries and verifies accessories for a product family, running physical compatibility checks.
|
|
1107
1406
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seeed-studio/solution-cli",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Installer for the Rust deployment engine and Solution CLI Agent skills (Work
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Installer for the Rust deployment engine and Solution CLI Agent skills (Doubao Work, WorkBuddy, Claude Code, Codex, CodeBuddy, Hermes)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/installer.js",
|
|
7
7
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "node scripts/bake-client-config.mjs",
|
|
24
24
|
"prepack": "node scripts/bake-client-config.mjs",
|
|
25
|
-
"test": "node --test
|
|
25
|
+
"test": "node --test",
|
|
26
26
|
"install:self": "node bin/cli.js install",
|
|
27
27
|
"status": "node bin/cli.js status",
|
|
28
28
|
"doctor": "node bin/cli.js doctor"
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"claude-code",
|
|
37
37
|
"codex",
|
|
38
38
|
"codebuddy",
|
|
39
|
+
"hermes",
|
|
39
40
|
"skills",
|
|
40
41
|
"edge-ai",
|
|
41
42
|
"jetson",
|