@huaqiu/dsh-tool-part-search 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/cordis.patch.yml +5 -0
- package/lib/index.d.mts +19 -0
- package/lib/index.mjs +281 -0
- package/package.json +40 -0
- package/src/index.ts +69 -0
- package/src/service.ts +57 -0
- package/src/tools.ts +230 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 深圳华秋智联股份有限公司
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @huaqiu/dsh-tool-part-search
|
|
2
|
+
|
|
3
|
+
Huaqiu PCB part search DSH tool plugin — **node half only**. Exposes the Huaqiu
|
|
4
|
+
public part-search API as four agent-visible tools:
|
|
5
|
+
|
|
6
|
+
| Tool | Operation |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| `search_hqsch_parts` | `PartSearchService.searchParts` |
|
|
9
|
+
| `get_hqsch_part` | `PartSearchService.getPart` |
|
|
10
|
+
| `get_hqsch_part_models` | `PartSearchService.getEdaModels` |
|
|
11
|
+
| `get_hqsch_supply_chain` | `PartSearchService.getSupplyChain` |
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
- **Phase 1** — the first published DSH plugin. It is the smallest vertical
|
|
16
|
+
slice that validates the whole packaging path: `package.json` →
|
|
17
|
+
`cordis.patch.yml` → plugin loading → `ctx.tools` → `defineTool` → npm
|
|
18
|
+
publish → stock DSH install → tool invocation.
|
|
19
|
+
- **`@huaqiu/part-search`** (published, public, unauthenticated) is the **single
|
|
20
|
+
implementation** of the Huaqiu part-search integration. This plugin only
|
|
21
|
+
adapts it into DSH tools — there is no HQ Edge, no HTTP proxy, no
|
|
22
|
+
`@hqedge/*` dependency.
|
|
23
|
+
- Node only: part search returns JSON, never renders UI → no client bundle.
|
|
24
|
+
- Self-contained: the plugin talks directly to `https://kiapi.eda.cn` (the
|
|
25
|
+
library's hardcoded upstream) with the global `fetch`.
|
|
26
|
+
|
|
27
|
+
## Files
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
src/
|
|
31
|
+
index.ts # plugin: name / inject ['tools'] / apply (registers 4 tools)
|
|
32
|
+
service.ts # thin PartSearchService adapter (the Huaqiu → DSH boundary)
|
|
33
|
+
tools.ts # the four defineTool definitions (the DSH tool adapter)
|
|
34
|
+
test/
|
|
35
|
+
index.test.ts # plugin shape + registration
|
|
36
|
+
tools.test.ts # arg mapping + render, against a stub service
|
|
37
|
+
service.test.ts # library wiring via a stub fetch
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm install
|
|
44
|
+
pnpm --filter @huaqiu/dsh-tool-part-search typecheck
|
|
45
|
+
pnpm --filter @huaqiu/dsh-tool-part-search test
|
|
46
|
+
pnpm --filter @huaqiu/dsh-tool-part-search build
|
|
47
|
+
pnpm --filter @huaqiu/dsh-tool-part-search pack --dry-run # release check
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Install / verify on a stock DSH
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
dsh plugin --profile web add /path/to/dsh-pcb-eda/packages/dsh-tool-part-search
|
|
54
|
+
dsh --profile web --dump-config
|
|
55
|
+
dsh web
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then ask the agent to `search_hqsch_parts` (e.g. query "STM32F103").
|
|
59
|
+
|
|
60
|
+
## Status
|
|
61
|
+
|
|
62
|
+
Phase 1 complete. Version 0.1.0 ready to publish.
|
package/cordis.patch.yml
ADDED
package/lib/index.d.mts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
//#region src/index.d.ts
|
|
3
|
+
/** Plugin id — matches package.json. */
|
|
4
|
+
declare const name = "@huaqiu/dsh-tool-part-search";
|
|
5
|
+
/** Cordis services this half depends on: the DSH node tool registry. */
|
|
6
|
+
declare const inject: readonly ["tools"];
|
|
7
|
+
/**
|
|
8
|
+
* Host plugin body — register the four agent-visible part-search tools.
|
|
9
|
+
*
|
|
10
|
+
* A single shared `PartSearchService` instance is created per plugin so every
|
|
11
|
+
* tool reuses the same client (and test/stub injection point).
|
|
12
|
+
*
|
|
13
|
+
* @param ctx - real cordis context (node side).
|
|
14
|
+
* @returns disposer — unregisters all four tools on plugin dispose. No
|
|
15
|
+
* duplicate tools can survive a reload.
|
|
16
|
+
*/
|
|
17
|
+
declare function apply(ctx: Context): () => void;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { apply, inject, name };
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { createPartSearchService } from "@huaqiu/part-search";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
//#region src/service.ts
|
|
4
|
+
/**
|
|
5
|
+
* Huaqiu part-search service adapter — the thin separation between the
|
|
6
|
+
* `@huaqiu/part-search` library and the DSH tool layer.
|
|
7
|
+
*
|
|
8
|
+
* The library (`PartSearchService`) is the single implementation of the Huaqiu
|
|
9
|
+
* public part-search API and returns the normalized domain model. This module
|
|
10
|
+
* only:
|
|
11
|
+
*
|
|
12
|
+
* - owns the client lifecycle (one shared service instance per plugin),
|
|
13
|
+
* - narrows the surface to what the four tools call,
|
|
14
|
+
* - stays DSH-free (no cordis imports) so it is trivially unit-testable.
|
|
15
|
+
*
|
|
16
|
+
* The upstream base URL is intentionally NOT configurable here: the library
|
|
17
|
+
* hardcodes `https://kiapi.eda.cn` and the whole point of Phase 1 is that the
|
|
18
|
+
* plugin is a self-contained, public, unauthenticated capability.
|
|
19
|
+
*
|
|
20
|
+
* @module @huaqiu/dsh-tool-part-search/service
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Create the Huaqiu part-search service used by all four tools.
|
|
24
|
+
*
|
|
25
|
+
* @param options - optional client options (language / timeout / fetch / logger).
|
|
26
|
+
* Omitted in production so the library defaults apply (global fetch, 15s
|
|
27
|
+
* timeout, zh). Tests pass a stub `fetch` or replace the returned instance.
|
|
28
|
+
* @returns a ready-to-use service.
|
|
29
|
+
*/
|
|
30
|
+
function createPartSearch(options) {
|
|
31
|
+
return createPartSearchService(options);
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/tools.ts
|
|
35
|
+
/**
|
|
36
|
+
* The four Huaqiu part-search DSH tools — the DSH tool adapter layer.
|
|
37
|
+
*
|
|
38
|
+
* This module is a **pure adapter**: every tool reads validated arguments,
|
|
39
|
+
* calls one `PartSearchServiceLike` operation (see `service.ts`), and returns
|
|
40
|
+
* the normalized domain model as its canonical JSON value. It never touches the
|
|
41
|
+
* wire protocol, `@huaqiu/part-search` internals, or any DSH host service.
|
|
42
|
+
*
|
|
43
|
+
* The agent-facing contracts (names, descriptions, snake_case parameters) are
|
|
44
|
+
* preserved verbatim from the original HQ Edge plugin so existing agent
|
|
45
|
+
* behaviors keep working after the migration.
|
|
46
|
+
*
|
|
47
|
+
* Error model: `PartSearchError` subclasses thrown by the service propagate to
|
|
48
|
+
* the DSH tool runtime, which surfaces the message to the model as a tool
|
|
49
|
+
* failure (same observable contract as the old HQ Edge proxy, which threw on
|
|
50
|
+
* upstream failure).
|
|
51
|
+
*
|
|
52
|
+
* @module @huaqiu/dsh-tool-part-search/tools
|
|
53
|
+
*/
|
|
54
|
+
/** Per-tool cooperative execution budget (ms). The upstream may be a fresh
|
|
55
|
+
* fetch (~15s); give headroom so `exec.signal` aborts gracefully. */
|
|
56
|
+
const TOOL_TIMEOUT_MS = 3e4;
|
|
57
|
+
/** Deterministic model content for every canonical part-search value. The value
|
|
58
|
+
* is already the normalized domain model from `@huaqiu/part-search`. */
|
|
59
|
+
function renderJson(_args, value) {
|
|
60
|
+
return [{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: JSON.stringify(value)
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The normalized domain model is plain JSON **except** that optional fields
|
|
67
|
+
* are present as `undefined`, which is not lossless JSON and fails the DSH
|
|
68
|
+
* runtime's canonical-value validation. The JSON round-trip strips
|
|
69
|
+
* `undefined` properties (and array holes → null) so the canonical value is
|
|
70
|
+
* always valid lossless JSON.
|
|
71
|
+
*/
|
|
72
|
+
function asJson(value) {
|
|
73
|
+
return JSON.parse(JSON.stringify(value));
|
|
74
|
+
}
|
|
75
|
+
/** The language param shared by detail / models lookups. */
|
|
76
|
+
const LANGUAGE_PARAM = {
|
|
77
|
+
type: "string",
|
|
78
|
+
enum: ["en", "zh"],
|
|
79
|
+
description: "Response language. Default \"zh\"."
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Build the four part-search tool definitions.
|
|
83
|
+
*
|
|
84
|
+
* @param service - the Huaqiu part-search service the tools call. Pass the
|
|
85
|
+
* shared instance from `index.ts` (or a stub in tests).
|
|
86
|
+
* @returns the four registry-ready tool definitions.
|
|
87
|
+
*/
|
|
88
|
+
function createPartSearchTools(service) {
|
|
89
|
+
return [
|
|
90
|
+
defineTool({
|
|
91
|
+
name: "search_hqsch_parts",
|
|
92
|
+
description: "Search electronic components / ICs / PCB parts by keyword through HQSCH (Huaqiu EDA). Returns candidate parts with MPN, manufacturer, package, description, and EDA-model availability flags. Use this when the user asks for part selection, component lookup, finding ICs, resistors, capacitors, or any electronic parts suitable for EDA/PCB design. By default only parts with available EDA models (schematic symbol / PCB footprint) are returned. Progressive retrieval is recommended: search_hqsch_parts → get_hqsch_part → get_hqsch_part_models / get_hqsch_supply_chain.",
|
|
93
|
+
parameters: {
|
|
94
|
+
query: {
|
|
95
|
+
type: "string",
|
|
96
|
+
required: true,
|
|
97
|
+
description: "Search keyword: a partial MPN (e.g. \"STM32F103\"), a description (\"32-bit microcontroller 72MHz\"), or a combined value (\"0402 10k resistor\"). Max 200 characters. Matching is fuzzy — related models may appear."
|
|
98
|
+
},
|
|
99
|
+
page: {
|
|
100
|
+
type: "integer",
|
|
101
|
+
description: "1-based page index. Default 1."
|
|
102
|
+
},
|
|
103
|
+
page_size: {
|
|
104
|
+
type: "integer",
|
|
105
|
+
description: "Page size, 1-50. Default 10."
|
|
106
|
+
},
|
|
107
|
+
require_eda_model: {
|
|
108
|
+
type: "boolean",
|
|
109
|
+
description: "When true (default), only return parts that have any EDA model. Set false to search across all parts (e.g. for research)."
|
|
110
|
+
},
|
|
111
|
+
requirements: {
|
|
112
|
+
type: "object",
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
description: "Fine-grained model requirements. All optional booleans: { symbol, footprint, model3d, simulation, supplier }.",
|
|
115
|
+
properties: {
|
|
116
|
+
symbol: {
|
|
117
|
+
type: "boolean",
|
|
118
|
+
description: "Require a schematic symbol."
|
|
119
|
+
},
|
|
120
|
+
footprint: {
|
|
121
|
+
type: "boolean",
|
|
122
|
+
description: "Require a PCB footprint."
|
|
123
|
+
},
|
|
124
|
+
model3d: {
|
|
125
|
+
type: "boolean",
|
|
126
|
+
description: "Require a 3D model."
|
|
127
|
+
},
|
|
128
|
+
simulation: {
|
|
129
|
+
type: "boolean",
|
|
130
|
+
description: "Require a simulation model."
|
|
131
|
+
},
|
|
132
|
+
supplier: {
|
|
133
|
+
type: "boolean",
|
|
134
|
+
description: "Require supplier/stock data."
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
language: LANGUAGE_PARAM
|
|
139
|
+
},
|
|
140
|
+
output: {
|
|
141
|
+
schema: { type: "json" },
|
|
142
|
+
render: renderJson
|
|
143
|
+
},
|
|
144
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
145
|
+
async execute(args) {
|
|
146
|
+
return asJson(await service.searchParts({
|
|
147
|
+
query: args.query,
|
|
148
|
+
page: args.page,
|
|
149
|
+
pageSize: args.page_size,
|
|
150
|
+
requireEdaModel: args.require_eda_model,
|
|
151
|
+
requirements: args.requirements,
|
|
152
|
+
language: args.language
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
}),
|
|
156
|
+
defineTool({
|
|
157
|
+
name: "get_hqsch_part",
|
|
158
|
+
description: "Get the canonical detailed part from HQSCH (Huaqiu EDA) by manufacturer id + MPN. Returns attributes, categories, documents (datasheets), images, EDA-model metadata (symbol/footprint/3D/simulation URLs), and tags. Use this after search_hqsch_parts to inspect a specific candidate. The part MUST be identified by both manufacturerId and mpn — partial identifiers are not accepted.",
|
|
159
|
+
parameters: {
|
|
160
|
+
manufacturer_id: {
|
|
161
|
+
type: "string",
|
|
162
|
+
required: true,
|
|
163
|
+
description: "Huaqiu manufacturer id (e.g. \"7189\" for STMicroelectronics)."
|
|
164
|
+
},
|
|
165
|
+
mpn: {
|
|
166
|
+
type: "string",
|
|
167
|
+
required: true,
|
|
168
|
+
description: "Manufacturer part number (e.g. \"STM32F410T8Y6TR\")."
|
|
169
|
+
},
|
|
170
|
+
language: LANGUAGE_PARAM
|
|
171
|
+
},
|
|
172
|
+
output: {
|
|
173
|
+
schema: { type: "json" },
|
|
174
|
+
render: renderJson
|
|
175
|
+
},
|
|
176
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
177
|
+
async execute(args) {
|
|
178
|
+
const identifier = toPartIdentifier(args);
|
|
179
|
+
return asJson(await service.getPart(identifier, args.language));
|
|
180
|
+
}
|
|
181
|
+
}),
|
|
182
|
+
defineTool({
|
|
183
|
+
name: "get_hqsch_part_models",
|
|
184
|
+
description: "Get EDA model metadata (schematic symbol / PCB footprint / 3D / simulation) for a HQSCH (Huaqiu EDA) part by manufacturer id + MPN. Returns URLs and format hints — does NOT download the model files. Use this when you need to know which models exist and where to fetch them, without retrieving the full part detail.",
|
|
185
|
+
parameters: {
|
|
186
|
+
manufacturer_id: {
|
|
187
|
+
type: "string",
|
|
188
|
+
required: true,
|
|
189
|
+
description: "Huaqiu manufacturer id (e.g. \"7189\" for STMicroelectronics)."
|
|
190
|
+
},
|
|
191
|
+
mpn: {
|
|
192
|
+
type: "string",
|
|
193
|
+
required: true,
|
|
194
|
+
description: "Manufacturer part number (e.g. \"STM32F410T8Y6TR\")."
|
|
195
|
+
},
|
|
196
|
+
language: LANGUAGE_PARAM
|
|
197
|
+
},
|
|
198
|
+
output: {
|
|
199
|
+
schema: { type: "json" },
|
|
200
|
+
render: renderJson
|
|
201
|
+
},
|
|
202
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
203
|
+
async execute(args) {
|
|
204
|
+
const identifier = toPartIdentifier(args);
|
|
205
|
+
return asJson(await service.getEdaModels(identifier, args.language));
|
|
206
|
+
}
|
|
207
|
+
}),
|
|
208
|
+
defineTool({
|
|
209
|
+
name: "get_hqsch_supply_chain",
|
|
210
|
+
description: "Get supply-chain offers (vendor, stock, MOQ, lead time, price breaks, distributor URL) for one or more HQSCH (Huaqiu EDA) parts by manufacturer id + MPN. Batched lookup is supported — pass an array of parts. Use this AFTER identifying a specific part via search_hqsch_parts / get_hqsch_part, when procurement or availability information is needed.",
|
|
211
|
+
parameters: { parts: {
|
|
212
|
+
type: "array",
|
|
213
|
+
required: true,
|
|
214
|
+
description: "Array of parts to look up supply-chain offers for.",
|
|
215
|
+
items: {
|
|
216
|
+
type: "object",
|
|
217
|
+
additionalProperties: false,
|
|
218
|
+
properties: {
|
|
219
|
+
manufacturer_id: {
|
|
220
|
+
type: "string",
|
|
221
|
+
required: true
|
|
222
|
+
},
|
|
223
|
+
mpn: {
|
|
224
|
+
type: "string",
|
|
225
|
+
required: true
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} },
|
|
230
|
+
output: {
|
|
231
|
+
schema: { type: "json" },
|
|
232
|
+
render: renderJson
|
|
233
|
+
},
|
|
234
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
235
|
+
async execute(args) {
|
|
236
|
+
const parts = args.parts.map((part) => ({
|
|
237
|
+
manufacturerId: part.manufacturer_id,
|
|
238
|
+
mpn: part.mpn
|
|
239
|
+
}));
|
|
240
|
+
return asJson(await service.getSupplyChain(parts));
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
/** Narrow the shared `manufacturer_id` + `mpn` args into a PartIdentifier. */
|
|
246
|
+
function toPartIdentifier(args) {
|
|
247
|
+
return {
|
|
248
|
+
manufacturerId: args.manufacturer_id,
|
|
249
|
+
mpn: args.mpn
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/index.ts
|
|
254
|
+
/** Plugin id — matches package.json. */
|
|
255
|
+
const name = "@huaqiu/dsh-tool-part-search";
|
|
256
|
+
/** Cordis services this half depends on: the DSH node tool registry. */
|
|
257
|
+
const inject = ["tools"];
|
|
258
|
+
/** Console tag for filtering in logs. */
|
|
259
|
+
const LOG_TAG = "[dsh-part-search]";
|
|
260
|
+
/**
|
|
261
|
+
* Host plugin body — register the four agent-visible part-search tools.
|
|
262
|
+
*
|
|
263
|
+
* A single shared `PartSearchService` instance is created per plugin so every
|
|
264
|
+
* tool reuses the same client (and test/stub injection point).
|
|
265
|
+
*
|
|
266
|
+
* @param ctx - real cordis context (node side).
|
|
267
|
+
* @returns disposer — unregisters all four tools on plugin dispose. No
|
|
268
|
+
* duplicate tools can survive a reload.
|
|
269
|
+
*/
|
|
270
|
+
function apply(ctx) {
|
|
271
|
+
if (!ctx.tools || typeof ctx.tools.register !== "function") throw new Error("@huaqiu/dsh-tool-part-search requires the DSH `tools` service (ctx.tools.register).");
|
|
272
|
+
const disposers = createPartSearchTools(createPartSearch()).map((tool) => ctx.tools.register(tool));
|
|
273
|
+
console.log(LOG_TAG, "registered agent tools", { tools: disposers.length });
|
|
274
|
+
return function dispose() {
|
|
275
|
+
for (const disposeTool of disposers) try {
|
|
276
|
+
disposeTool();
|
|
277
|
+
} catch {}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
export { apply, inject, name };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huaqiu/dsh-tool-part-search",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./lib/index.mjs",
|
|
6
|
+
"types": "./lib/index.d.mts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/index.d.mts",
|
|
10
|
+
"default": "./lib/index.mjs"
|
|
11
|
+
},
|
|
12
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"dsh": {
|
|
16
|
+
"bundle": {
|
|
17
|
+
"patch": "./cordis.patch.yml"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
22
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.2.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@huaqiu/part-search": "^0.2.0"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"lib",
|
|
29
|
+
"src",
|
|
30
|
+
"cordis.patch.yml"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Huaqiu EDA Part Search DSH tool plugin (node half) — `@huaqiu/dsh-tool-part-search`.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the Huaqiu public part-search capability as four agent-visible tools:
|
|
5
|
+
*
|
|
6
|
+
* search_hqsch_parts → PartSearchService.searchParts
|
|
7
|
+
* get_hqsch_part → PartSearchService.getPart
|
|
8
|
+
* get_hqsch_part_models → PartSearchService.getEdaModels
|
|
9
|
+
* get_hqsch_supply_chain → PartSearchService.getSupplyChain
|
|
10
|
+
*
|
|
11
|
+
* ── Architectural boundary (migration plan §1/§8/§20) ───────────────────────
|
|
12
|
+
* Phase 1 makes this the first published DSH plugin and the smallest vertical
|
|
13
|
+
* slice: package.json → cordis.patch.yml → plugin loading → ctx.tools →
|
|
14
|
+
* defineTool → npm publish → stock DSH install → tool invocation. The Huaqiu
|
|
15
|
+
* integration is owned exactly once by the `@huaqiu/part-search` library; this
|
|
16
|
+
* plugin calls that library directly (no HQ Edge, no HTTP proxy, no
|
|
17
|
+
* `@hqedge/*` dependency).
|
|
18
|
+
*
|
|
19
|
+
* ── Why no client half ───────────────────────────────────────────────────────
|
|
20
|
+
* Part search is a node-only capability: it returns JSON, never renders UI.
|
|
21
|
+
* `inject = ['tools']` resolves the DSH node tool registry.
|
|
22
|
+
*
|
|
23
|
+
* @module @huaqiu/dsh-tool-part-search
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
27
|
+
import { createPartSearch } from './service.js'
|
|
28
|
+
import { createPartSearchTools } from './tools.js'
|
|
29
|
+
|
|
30
|
+
/** Plugin id — matches package.json. */
|
|
31
|
+
export const name = '@huaqiu/dsh-tool-part-search'
|
|
32
|
+
|
|
33
|
+
/** Cordis services this half depends on: the DSH node tool registry. */
|
|
34
|
+
export const inject = ['tools'] as const
|
|
35
|
+
|
|
36
|
+
/** Console tag for filtering in logs. */
|
|
37
|
+
const LOG_TAG = '[dsh-part-search]'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Host plugin body — register the four agent-visible part-search tools.
|
|
41
|
+
*
|
|
42
|
+
* A single shared `PartSearchService` instance is created per plugin so every
|
|
43
|
+
* tool reuses the same client (and test/stub injection point).
|
|
44
|
+
*
|
|
45
|
+
* @param ctx - real cordis context (node side).
|
|
46
|
+
* @returns disposer — unregisters all four tools on plugin dispose. No
|
|
47
|
+
* duplicate tools can survive a reload.
|
|
48
|
+
*/
|
|
49
|
+
export function apply(ctx: Context): () => void {
|
|
50
|
+
if (!ctx.tools || typeof ctx.tools.register !== 'function') {
|
|
51
|
+
throw new Error('@huaqiu/dsh-tool-part-search requires the DSH `tools` service (ctx.tools.register).')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const service = createPartSearch()
|
|
55
|
+
const disposers = createPartSearchTools(service).map((tool) => ctx.tools.register(tool))
|
|
56
|
+
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
58
|
+
console.log(LOG_TAG, 'registered agent tools', { tools: disposers.length })
|
|
59
|
+
|
|
60
|
+
return function dispose() {
|
|
61
|
+
for (const disposeTool of disposers) {
|
|
62
|
+
try {
|
|
63
|
+
disposeTool()
|
|
64
|
+
} catch {
|
|
65
|
+
// One failing unregister must not hide the others.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Huaqiu part-search service adapter — the thin separation between the
|
|
3
|
+
* `@huaqiu/part-search` library and the DSH tool layer.
|
|
4
|
+
*
|
|
5
|
+
* The library (`PartSearchService`) is the single implementation of the Huaqiu
|
|
6
|
+
* public part-search API and returns the normalized domain model. This module
|
|
7
|
+
* only:
|
|
8
|
+
*
|
|
9
|
+
* - owns the client lifecycle (one shared service instance per plugin),
|
|
10
|
+
* - narrows the surface to what the four tools call,
|
|
11
|
+
* - stays DSH-free (no cordis imports) so it is trivially unit-testable.
|
|
12
|
+
*
|
|
13
|
+
* The upstream base URL is intentionally NOT configurable here: the library
|
|
14
|
+
* hardcodes `https://kiapi.eda.cn` and the whole point of Phase 1 is that the
|
|
15
|
+
* plugin is a self-contained, public, unauthenticated capability.
|
|
16
|
+
*
|
|
17
|
+
* @module @huaqiu/dsh-tool-part-search/service
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
createPartSearchService,
|
|
22
|
+
type EdaModels,
|
|
23
|
+
type Part,
|
|
24
|
+
type PartIdentifier,
|
|
25
|
+
type PartSearchClientOptions,
|
|
26
|
+
type PartSearchPage,
|
|
27
|
+
type SearchPartsOptions,
|
|
28
|
+
type SupplyOffer,
|
|
29
|
+
} from '@huaqiu/part-search'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The operations the four DSH tools need. Declared as a structural interface so
|
|
33
|
+
* tests can inject a stub without touching the network.
|
|
34
|
+
*/
|
|
35
|
+
export interface PartSearchServiceLike {
|
|
36
|
+
searchParts(options: SearchPartsOptions): Promise<PartSearchPage>
|
|
37
|
+
getPart(input: PartIdentifier, language?: string): Promise<Part>
|
|
38
|
+
getEdaModels(input: PartIdentifier, language?: string): Promise<EdaModels>
|
|
39
|
+
getSupplyChain(parts: readonly PartIdentifier[]): Promise<SupplyOffer[]>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Options for {@link createPartSearchService}. */
|
|
43
|
+
export type PartSearchServiceOptions = PartSearchClientOptions
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create the Huaqiu part-search service used by all four tools.
|
|
47
|
+
*
|
|
48
|
+
* @param options - optional client options (language / timeout / fetch / logger).
|
|
49
|
+
* Omitted in production so the library defaults apply (global fetch, 15s
|
|
50
|
+
* timeout, zh). Tests pass a stub `fetch` or replace the returned instance.
|
|
51
|
+
* @returns a ready-to-use service.
|
|
52
|
+
*/
|
|
53
|
+
export function createPartSearch(options?: PartSearchServiceOptions): PartSearchServiceLike {
|
|
54
|
+
return createPartSearchService(options)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type { Part, PartIdentifier, PartSearchPage, SearchPartsOptions, SupplyOffer }
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four Huaqiu part-search DSH tools — the DSH tool adapter layer.
|
|
3
|
+
*
|
|
4
|
+
* This module is a **pure adapter**: every tool reads validated arguments,
|
|
5
|
+
* calls one `PartSearchServiceLike` operation (see `service.ts`), and returns
|
|
6
|
+
* the normalized domain model as its canonical JSON value. It never touches the
|
|
7
|
+
* wire protocol, `@huaqiu/part-search` internals, or any DSH host service.
|
|
8
|
+
*
|
|
9
|
+
* The agent-facing contracts (names, descriptions, snake_case parameters) are
|
|
10
|
+
* preserved verbatim from the original HQ Edge plugin so existing agent
|
|
11
|
+
* behaviors keep working after the migration.
|
|
12
|
+
*
|
|
13
|
+
* Error model: `PartSearchError` subclasses thrown by the service propagate to
|
|
14
|
+
* the DSH tool runtime, which surfaces the message to the model as a tool
|
|
15
|
+
* failure (same observable contract as the old HQ Edge proxy, which threw on
|
|
16
|
+
* upstream failure).
|
|
17
|
+
*
|
|
18
|
+
* @module @huaqiu/dsh-tool-part-search/tools
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
22
|
+
import type { PartIdentifier } from '@huaqiu/part-search'
|
|
23
|
+
import type { PartSearchServiceLike } from './service.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Structural alias of the DSH `JsonValue` (dsh-session). Kept local so the
|
|
27
|
+
* plugin does not need `@deepseek-ai/dsh-session` as a direct dependency for
|
|
28
|
+
* a type only; the alias is structurally identical to `JsonValue`, which is
|
|
29
|
+
* what `defineTool`'s `{ type: 'json' }` output infers.
|
|
30
|
+
*/
|
|
31
|
+
type Json = string | number | boolean | null | Json[] | { [key: string]: Json }
|
|
32
|
+
|
|
33
|
+
/** Per-tool cooperative execution budget (ms). The upstream may be a fresh
|
|
34
|
+
* fetch (~15s); give headroom so `exec.signal` aborts gracefully. */
|
|
35
|
+
const TOOL_TIMEOUT_MS = 30_000
|
|
36
|
+
|
|
37
|
+
/** Deterministic model content for every canonical part-search value. The value
|
|
38
|
+
* is already the normalized domain model from `@huaqiu/part-search`. */
|
|
39
|
+
function renderJson(_args: unknown, value: unknown) {
|
|
40
|
+
return [{ type: 'text' as const, text: JSON.stringify(value) }]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The normalized domain model is plain JSON **except** that optional fields
|
|
45
|
+
* are present as `undefined`, which is not lossless JSON and fails the DSH
|
|
46
|
+
* runtime's canonical-value validation. The JSON round-trip strips
|
|
47
|
+
* `undefined` properties (and array holes → null) so the canonical value is
|
|
48
|
+
* always valid lossless JSON.
|
|
49
|
+
*/
|
|
50
|
+
function asJson<T>(value: T): Json {
|
|
51
|
+
return JSON.parse(JSON.stringify(value)) as Json
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The language param shared by detail / models lookups. */
|
|
55
|
+
const LANGUAGE_PARAM = {
|
|
56
|
+
type: 'string',
|
|
57
|
+
enum: ['en', 'zh'],
|
|
58
|
+
description: 'Response language. Default "zh".',
|
|
59
|
+
} as const
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build the four part-search tool definitions.
|
|
63
|
+
*
|
|
64
|
+
* @param service - the Huaqiu part-search service the tools call. Pass the
|
|
65
|
+
* shared instance from `index.ts` (or a stub in tests).
|
|
66
|
+
* @returns the four registry-ready tool definitions.
|
|
67
|
+
*/
|
|
68
|
+
export function createPartSearchTools(service: PartSearchServiceLike) {
|
|
69
|
+
return [
|
|
70
|
+
defineTool({
|
|
71
|
+
name: 'search_hqsch_parts',
|
|
72
|
+
description:
|
|
73
|
+
'Search electronic components / ICs / PCB parts by keyword through HQSCH (Huaqiu EDA). ' +
|
|
74
|
+
'Returns candidate parts with MPN, manufacturer, package, description, and EDA-model ' +
|
|
75
|
+
'availability flags. Use this when the user asks for part selection, component lookup, ' +
|
|
76
|
+
'finding ICs, resistors, capacitors, or any electronic parts suitable for EDA/PCB design. ' +
|
|
77
|
+
'By default only parts with available EDA models (schematic symbol / PCB footprint) are ' +
|
|
78
|
+
'returned. Progressive retrieval is recommended: search_hqsch_parts → get_hqsch_part → ' +
|
|
79
|
+
'get_hqsch_part_models / get_hqsch_supply_chain.',
|
|
80
|
+
parameters: {
|
|
81
|
+
query: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
required: true,
|
|
84
|
+
description:
|
|
85
|
+
'Search keyword: a partial MPN (e.g. "STM32F103"), a description ' +
|
|
86
|
+
'("32-bit microcontroller 72MHz"), or a combined value ("0402 10k resistor"). ' +
|
|
87
|
+
'Max 200 characters. Matching is fuzzy — related models may appear.',
|
|
88
|
+
},
|
|
89
|
+
page: {
|
|
90
|
+
type: 'integer',
|
|
91
|
+
description: '1-based page index. Default 1.',
|
|
92
|
+
},
|
|
93
|
+
page_size: {
|
|
94
|
+
type: 'integer',
|
|
95
|
+
description: 'Page size, 1-50. Default 10.',
|
|
96
|
+
},
|
|
97
|
+
require_eda_model: {
|
|
98
|
+
type: 'boolean',
|
|
99
|
+
description:
|
|
100
|
+
'When true (default), only return parts that have any EDA model. ' +
|
|
101
|
+
'Set false to search across all parts (e.g. for research).',
|
|
102
|
+
},
|
|
103
|
+
requirements: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
description:
|
|
107
|
+
'Fine-grained model requirements. All optional booleans: ' +
|
|
108
|
+
'{ symbol, footprint, model3d, simulation, supplier }.',
|
|
109
|
+
properties: {
|
|
110
|
+
symbol: { type: 'boolean', description: 'Require a schematic symbol.' },
|
|
111
|
+
footprint: { type: 'boolean', description: 'Require a PCB footprint.' },
|
|
112
|
+
model3d: { type: 'boolean', description: 'Require a 3D model.' },
|
|
113
|
+
simulation: { type: 'boolean', description: 'Require a simulation model.' },
|
|
114
|
+
supplier: { type: 'boolean', description: 'Require supplier/stock data.' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
language: LANGUAGE_PARAM,
|
|
118
|
+
},
|
|
119
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
120
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
121
|
+
async execute(args) {
|
|
122
|
+
const page = await service.searchParts({
|
|
123
|
+
query: args.query,
|
|
124
|
+
page: args.page,
|
|
125
|
+
pageSize: args.page_size,
|
|
126
|
+
requireEdaModel: args.require_eda_model,
|
|
127
|
+
requirements: args.requirements,
|
|
128
|
+
language: args.language,
|
|
129
|
+
})
|
|
130
|
+
return asJson(page)
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
defineTool({
|
|
135
|
+
name: 'get_hqsch_part',
|
|
136
|
+
description:
|
|
137
|
+
'Get the canonical detailed part from HQSCH (Huaqiu EDA) by manufacturer id + MPN. ' +
|
|
138
|
+
'Returns attributes, categories, documents (datasheets), images, EDA-model metadata ' +
|
|
139
|
+
'(symbol/footprint/3D/simulation URLs), and tags. Use this after ' +
|
|
140
|
+
'search_hqsch_parts to inspect a specific candidate. The part MUST be identified by ' +
|
|
141
|
+
'both manufacturerId and mpn — partial identifiers are not accepted.',
|
|
142
|
+
parameters: {
|
|
143
|
+
manufacturer_id: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
required: true,
|
|
146
|
+
description: 'Huaqiu manufacturer id (e.g. "7189" for STMicroelectronics).',
|
|
147
|
+
},
|
|
148
|
+
mpn: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
required: true,
|
|
151
|
+
description: 'Manufacturer part number (e.g. "STM32F410T8Y6TR").',
|
|
152
|
+
},
|
|
153
|
+
language: LANGUAGE_PARAM,
|
|
154
|
+
},
|
|
155
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
156
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
157
|
+
async execute(args) {
|
|
158
|
+
const identifier = toPartIdentifier(args)
|
|
159
|
+
return asJson(await service.getPart(identifier, args.language))
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
|
|
163
|
+
defineTool({
|
|
164
|
+
name: 'get_hqsch_part_models',
|
|
165
|
+
description:
|
|
166
|
+
'Get EDA model metadata (schematic symbol / PCB footprint / 3D / simulation) for a ' +
|
|
167
|
+
'HQSCH (Huaqiu EDA) part by manufacturer id + MPN. Returns URLs and format hints — ' +
|
|
168
|
+
'does NOT download the model files. Use this when you need to know which models exist ' +
|
|
169
|
+
'and where to fetch them, without retrieving the full part detail.',
|
|
170
|
+
parameters: {
|
|
171
|
+
manufacturer_id: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
required: true,
|
|
174
|
+
description: 'Huaqiu manufacturer id (e.g. "7189" for STMicroelectronics).',
|
|
175
|
+
},
|
|
176
|
+
mpn: {
|
|
177
|
+
type: 'string',
|
|
178
|
+
required: true,
|
|
179
|
+
description: 'Manufacturer part number (e.g. "STM32F410T8Y6TR").',
|
|
180
|
+
},
|
|
181
|
+
language: LANGUAGE_PARAM,
|
|
182
|
+
},
|
|
183
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
184
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
185
|
+
async execute(args) {
|
|
186
|
+
const identifier = toPartIdentifier(args)
|
|
187
|
+
return asJson(await service.getEdaModels(identifier, args.language))
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
|
|
191
|
+
defineTool({
|
|
192
|
+
name: 'get_hqsch_supply_chain',
|
|
193
|
+
description:
|
|
194
|
+
'Get supply-chain offers (vendor, stock, MOQ, lead time, price breaks, distributor ' +
|
|
195
|
+
'URL) for one or more HQSCH (Huaqiu EDA) parts by manufacturer id + MPN. Batched ' +
|
|
196
|
+
'lookup is supported — pass an array of parts. Use this AFTER identifying a specific ' +
|
|
197
|
+
'part via search_hqsch_parts / get_hqsch_part, when procurement or availability ' +
|
|
198
|
+
'information is needed.',
|
|
199
|
+
parameters: {
|
|
200
|
+
parts: {
|
|
201
|
+
type: 'array',
|
|
202
|
+
required: true,
|
|
203
|
+
description: 'Array of parts to look up supply-chain offers for.',
|
|
204
|
+
items: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
additionalProperties: false,
|
|
207
|
+
properties: {
|
|
208
|
+
manufacturer_id: { type: 'string', required: true },
|
|
209
|
+
mpn: { type: 'string', required: true },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
215
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
216
|
+
async execute(args) {
|
|
217
|
+
const parts: PartIdentifier[] = args.parts.map((part) => ({
|
|
218
|
+
manufacturerId: part.manufacturer_id,
|
|
219
|
+
mpn: part.mpn,
|
|
220
|
+
}))
|
|
221
|
+
return asJson(await service.getSupplyChain(parts))
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Narrow the shared `manufacturer_id` + `mpn` args into a PartIdentifier. */
|
|
228
|
+
function toPartIdentifier(args: { manufacturer_id: string; mpn: string }): PartIdentifier {
|
|
229
|
+
return { manufacturerId: args.manufacturer_id, mpn: args.mpn }
|
|
230
|
+
}
|