@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 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.
@@ -0,0 +1,5 @@
1
+ # DSH bundle patch: inserts the Huaqiu part-search tool plugin into a profile's layer stack.
2
+ - insert:
3
+ - id: huaqiu-tool-part-search
4
+ name: '@huaqiu/dsh-tool-part-search'
5
+ inject: ['tools']
@@ -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
+ }