@huaqiu/dsh-tool-quote 0.0.0-stage → 0.5.3

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.
@@ -0,0 +1,5 @@
1
+ # DSH bundle patch: inserts the Huaqiu quote tool plugin into a profile's layer stack.
2
+ - insert:
3
+ - id: huaqiu-tool-quote
4
+ name: '@huaqiu/dsh-tool-quote'
5
+ inject: ['tools', 'hqEdge']
@@ -0,0 +1,64 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/config.d.ts
3
+ /**
4
+ * Runtime config resolution for `@huaqiu/dsh-tool-quote`.
5
+ *
6
+ * The plugin never opens a direct connection to the price providers — it only
7
+ * talks to `hq-edge`, which owns the quote capability (hq.fab.v1.QuoteService
8
+ * bridged over HTTP, see apps/server/src/routes/quote.ts on hq-edge). The base
9
+ * URL is delivered by the hq-edge supervisor as overlay config
10
+ * (`hqEdgeBaseUrl`), with `HQ_EDGE_BASE_URL` as env fallback — the same
11
+ * convention as `@huaqiu/dsh-eda-host` / `@huaqiu/dsh-auth`.
12
+ *
13
+ * @module
14
+ */
15
+ interface QuoteToolConfig {
16
+ /** HQ Edge base URL, e.g. "http://localhost:18080". Absent → no host. */
17
+ hqEdgeBaseUrl?: string;
18
+ /** Path prefix on the host; default "/api/v1/quote". */
19
+ quotePathPrefix?: string;
20
+ /** End-to-end budget for one quote request, in milliseconds (default 30_000). */
21
+ requestTimeoutMs?: number;
22
+ }
23
+ //#endregion
24
+ //#region src/client.d.ts
25
+ /** Semantic failure of a quote request, mirroring hq-edge's envelope kinds. */
26
+ declare class QuoteToolError extends Error {
27
+ readonly kind: string;
28
+ readonly status: number;
29
+ readonly detail?: string | undefined;
30
+ readonly code?: string | number | undefined;
31
+ constructor(kind: string, message: string, status: number, detail?: string | undefined, code?: string | number | undefined);
32
+ }
33
+ //#endregion
34
+ //#region src/index.d.ts
35
+ /** Plugin id — matches package.json. */
36
+ declare const name = "@huaqiu/dsh-tool-quote";
37
+ /** Cordis services this half depends on: the node tool registry + hq-edge
38
+ * bridge (REQUIRED — quote cannot work standalone). */
39
+ declare const inject: readonly ["hqEdge", "tools"];
40
+ declare module '@deepseek-ai/cordis' {
41
+ interface Context {
42
+ /**
43
+ * Provided by the hq-edge `edge-bridge` plugin (the HOST). `baseUrl` is
44
+ * the loopback HQ Edge endpoint, e.g. "http://localhost:18080". Read
45
+ * lazily on every request; REQUIRED for this plugin to load.
46
+ */
47
+ hqEdge: {
48
+ baseUrl?: string;
49
+ };
50
+ }
51
+ }
52
+ /**
53
+ * Host plugin body — register the two quote tools.
54
+ *
55
+ * The HQ Edge endpoint is resolved lazily at request time from the
56
+ * `ctx.hqEdge` service (edge-bridge), falling back to the overlay config /
57
+ * `HQ_EDGE_BASE_URL` env (same convention as `@huaqiu/dsh-eda-host`).
58
+ *
59
+ * @param ctx - real cordis context (node side).
60
+ * @returns disposer — unregisters the tools on plugin dispose.
61
+ */
62
+ declare function apply(ctx: Context, config?: Partial<QuoteToolConfig>): () => void;
63
+ //#endregion
64
+ export { type QuoteToolConfig, QuoteToolError, apply, inject, name };
package/lib/index.mjs ADDED
@@ -0,0 +1,384 @@
1
+ import { getLogger } from "@huaqiu/dsh-plugin-log";
2
+ import { defineTool } from "@deepseek-ai/dsh-tools";
3
+ const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
4
+ function resolveQuoteToolConfig(config, env = process.env) {
5
+ const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? "";
6
+ const pathPrefix = config?.quotePathPrefix ?? env.HQ_EDGE_QUOTE_PATH ?? "/api/v1/quote";
7
+ const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS;
8
+ const timeout = Number.parseInt(String(timeoutRaw ?? ""), 10);
9
+ return {
10
+ hqEdgeBaseUrl: baseUrl,
11
+ quotePathPrefix: pathPrefix,
12
+ requestTimeoutMs: Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS
13
+ };
14
+ }
15
+ /** True when a host base URL is available (host mode). */
16
+ function hasHost(config) {
17
+ return typeof config.hqEdgeBaseUrl === "string" && config.hqEdgeBaseUrl.trim().length > 0;
18
+ }
19
+ /** Build the absolute URL for one quote kind route ("pcb" | "smt"). */
20
+ function quoteUrlOf(config, kind) {
21
+ return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.quotePathPrefix ?? "/api/v1/quote").replace(/^\/+|\/+$/g, "")}/${kind}`;
22
+ }
23
+ //#endregion
24
+ //#region src/client.ts
25
+ /** Semantic failure of a quote request, mirroring hq-edge's envelope kinds. */
26
+ var QuoteToolError = class extends Error {
27
+ kind;
28
+ status;
29
+ detail;
30
+ code;
31
+ constructor(kind, message, status, detail, code) {
32
+ super(message);
33
+ this.kind = kind;
34
+ this.status = status;
35
+ this.detail = detail;
36
+ this.code = code;
37
+ this.name = "QuoteToolError";
38
+ }
39
+ };
40
+ /** HTTP status → semantic kind (hq-edge envelope, mirrors eda-host mapping). */
41
+ function statusToKind(status) {
42
+ if (status === 400) return "VALIDATION";
43
+ if (status === 501) return "UNIMPLEMENTED";
44
+ if (status === 503) return "UNAVAILABLE";
45
+ if (status === 504) return "DEADLINE_EXCEEDED";
46
+ return "INTERNAL";
47
+ }
48
+ function isAbortError(err) {
49
+ const name = err?.name;
50
+ return name === "AbortError" || name === "TimeoutError";
51
+ }
52
+ function resolveSignal(deadlineMs, caller) {
53
+ const signals = [caller, deadlineMs > 0 ? AbortSignal.timeout(deadlineMs) : void 0].filter((s) => Boolean(s));
54
+ if (signals.length === 0) return void 0;
55
+ if (signals.length === 1) return signals[0];
56
+ const anyFn = AbortSignal.any;
57
+ return typeof anyFn === "function" ? anyFn.call(AbortSignal, signals) : signals[0];
58
+ }
59
+ function createQuoteToolClient(config, deps = {}) {
60
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
61
+ const resolveBaseUrl = () => {
62
+ const fromService = deps.baseUrlResolver?.();
63
+ const base = fromService?.trim().length ? fromService : config.hqEdgeBaseUrl;
64
+ if (!base || base.trim().length === 0) throw new QuoteToolError("FAILED_PRECONDITION", "quote: no hq-edge base URL available (edge-bridge service or HQ_EDGE_BASE_URL)", 412);
65
+ return base.trim();
66
+ };
67
+ const request = async (kind, payload, options = {}) => {
68
+ const base = resolveBaseUrl();
69
+ const url = quoteUrlOf({
70
+ ...config,
71
+ hqEdgeBaseUrl: base
72
+ }, kind);
73
+ const signal = resolveSignal(config.requestTimeoutMs ?? 0, options.signal);
74
+ try {
75
+ const response = await fetchImpl(url, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify(payload),
79
+ signal
80
+ });
81
+ const text = await response.text();
82
+ let body;
83
+ try {
84
+ body = text.length > 0 ? JSON.parse(text) : null;
85
+ } catch {
86
+ body = null;
87
+ }
88
+ if (!response.ok) {
89
+ const envelope = body && typeof body === "object" && !Array.isArray(body) ? body : {};
90
+ throw new QuoteToolError(typeof envelope.error === "string" ? envelope.error : statusToKind(response.status), typeof envelope.detail === "string" ? envelope.detail : "quote request failed", response.status, typeof envelope.detail === "string" ? envelope.detail : void 0, typeof envelope.code === "string" || typeof envelope.code === "number" ? envelope.code : void 0);
91
+ }
92
+ return body;
93
+ } catch (err) {
94
+ if (err instanceof QuoteToolError) throw err;
95
+ if (isAbortError(err)) throw new QuoteToolError("DEADLINE_EXCEEDED", "quote request timed out", 504);
96
+ throw new QuoteToolError("INTERNAL", `quote request failed: ${String(err)}`, 500);
97
+ }
98
+ };
99
+ return {
100
+ quotePcb: (payload, options) => request("pcb", payload, options),
101
+ quoteSmt: (payload, options) => request("smt", payload, options)
102
+ };
103
+ }
104
+ //#endregion
105
+ //#region src/tools.ts
106
+ /**
107
+ * Agent tools for `@huaqiu/dsh-tool-quote`.
108
+ *
109
+ * quote_pcb real-time PCB fabrication price for the requested region
110
+ * quote_smt real-time SMT assembly price (single-side / double-side)
111
+ *
112
+ * The tools are pure pass-throughs: they read validated arguments, build the
113
+ * canonical quote request (matching hq.fab.v1.QuoteService / the hq-edge
114
+ * router contract), call hq-edge via the client, and return the normalized
115
+ * QuotePrice as lossless JSON. Errors carry a semantic `kind`
116
+ * (FAILED_PRECONDITION / VALIDATION / UNIMPLEMENTED / UPSTREAM_REJECTED /
117
+ * DEADLINE_EXCEEDED / INTERNAL) — the agent sees why a quote failed, not a
118
+ * bare HTTP status.
119
+ *
120
+ * Note on enums: only the enumerated choices are accepted, mirroring the
121
+ * proto/type definitions on hq-edge. Unknown fields are rejected upstream
122
+ * (Zod .strict()), so the tools never send unsupported keys.
123
+ *
124
+ * @module
125
+ */
126
+ function asJson(value) {
127
+ return JSON.parse(JSON.stringify(value));
128
+ }
129
+ function renderJson(_args, value) {
130
+ return [{
131
+ type: "text",
132
+ text: JSON.stringify(value)
133
+ }];
134
+ }
135
+ /** Pick the EDA derivation flags (only when explicitly true). */
136
+ function edaOf(args) {
137
+ const eda = {};
138
+ if (args.derive_board_size === true) eda.deriveBoardSize = true;
139
+ if (args.derive_board_stackup === true) eda.deriveBoardStackup = true;
140
+ return Object.keys(eda).length > 0 ? eda : void 0;
141
+ }
142
+ function createQuoteTools(env) {
143
+ const { client } = env;
144
+ /** Build the canonical PCB form from the flat tool args. */
145
+ const pcbFormOf = (a) => {
146
+ const form = {};
147
+ if (a.bcount !== void 0) form.bcount = a.bcount;
148
+ if (a.blayer !== void 0) form.blayer = a.blayer;
149
+ if (a.blength !== void 0) form.blength = a.blength;
150
+ if (a.bwidth !== void 0) form.bwidth = a.bwidth;
151
+ if (a.board_tg !== void 0) form.boardTg = a.board_tg;
152
+ if (a.color !== void 0) form.color = a.color;
153
+ if (a.cover !== void 0) form.cover = a.cover;
154
+ if (a.spray !== void 0) form.spray = a.spray;
155
+ return Object.keys(form).length > 0 ? form : void 0;
156
+ };
157
+ /** Optional EDA-host derivation flags shared by both tools. */
158
+ const edaFlagProps = {
159
+ derive_board_size: {
160
+ type: "boolean",
161
+ description: "When true, ask hq-edge to derive board length/width from the current PCB board (approximate bounding box of footprints; only when a board is open in the host)."
162
+ },
163
+ derive_board_stackup: {
164
+ type: "boolean",
165
+ description: "NOT SUPPORTED (explicit capability gap): hq-edge has no board-properties source, so stack-up is never derived or fabricated. Provide blayer/bheight explicitly instead."
166
+ },
167
+ include_raw_response: {
168
+ type: "boolean",
169
+ description: "Include the bounded raw upstream payload for diagnostics (default false)."
170
+ }
171
+ };
172
+ return [defineTool({
173
+ name: "quote_pcb",
174
+ description: "Get a real-time PCB fabrication price quote (multi-layer, panel pricing, delivery options) for the requested region through Huaqiu/NextPCB. Use when the user asks how much a PCB would cost, \"报价\", \"价格\", delivery time estimates, or comparing fab options. Returns the itemized price breakdown, total price, and delivery alternatives (name + build time in days + total price) with currency symbol (¥ CN / ¥ intl). When derive_board_size / derive_board_stackup is set, the response also carries the effective quote form: EDA-extracted board facts (length/width from the board outline, layer count + thickness from the stack-up in KiCad) merged with the caller overrides — plus server defaults applied on the wire (e.g. bcount default 5). Inspect \"form\" in the result, adjust any parameter the user wants to change (e.g. color, blayer, board_tg), and re-query with the corrected form rather than guessing values. Progressive usage: quote_pcb for PCB fabrication, quote_smt for assembly, then the user places the order in the EDA host. This is a quote-only query — no order is created.",
175
+ parameters: {
176
+ region: {
177
+ type: "string",
178
+ enum: [
179
+ "cn",
180
+ "eu_us",
181
+ "jp"
182
+ ],
183
+ required: true,
184
+ description: "Pricing region. \"cn\" = Huaqiu China (eda.cn, ¥), \"eu_us\" = NextPCB EU/US, \"jp\" = NextPCB Japan (both ¥)."
185
+ },
186
+ bcount: {
187
+ type: "integer",
188
+ description: "Panel quantity (boards per panel), >= 1. Default 5."
189
+ },
190
+ blayer: {
191
+ type: "integer",
192
+ description: "PCB layer count, 1-64. Default 4."
193
+ },
194
+ blength: {
195
+ type: "number",
196
+ description: "Board length in mm, > 0 (canonical mm; converted to cm on the wire)."
197
+ },
198
+ bwidth: {
199
+ type: "number",
200
+ description: "Board width in mm, > 0."
201
+ },
202
+ board_tg: {
203
+ type: "string",
204
+ enum: [
205
+ "tg130",
206
+ "tg150",
207
+ "tg170"
208
+ ],
209
+ description: "Glass transition temperature grade (4+ layers only)."
210
+ },
211
+ color: {
212
+ type: "string",
213
+ enum: [
214
+ "green",
215
+ "red",
216
+ "yellow",
217
+ "blue",
218
+ "white",
219
+ "black",
220
+ "matte_black"
221
+ ],
222
+ description: "Solder mask color. Default green."
223
+ },
224
+ cover: {
225
+ type: "string",
226
+ enum: [
227
+ "covered",
228
+ "exposed",
229
+ "solder_mask_plug",
230
+ "non_conductive_fill"
231
+ ],
232
+ description: "Via process (tenting / open / solder-mask-plug / non-conductive fill). Default covered."
233
+ },
234
+ spray: {
235
+ type: "string",
236
+ enum: [
237
+ "hasl_lead",
238
+ "hasl_lf",
239
+ "enig",
240
+ "osp"
241
+ ],
242
+ description: "Surface finish. Default HASL (lead)."
243
+ },
244
+ ...edaFlagProps
245
+ },
246
+ output: {
247
+ schema: { type: "json" },
248
+ render: renderJson
249
+ },
250
+ async execute(args, exec) {
251
+ const a = args;
252
+ const payload = {
253
+ region: a.region,
254
+ form: pcbFormOf(a),
255
+ eda: edaOf(a),
256
+ includeRawResponse: a.include_raw_response === true
257
+ };
258
+ return asJson(await client.quotePcb(payload, { signal: exec.signal }));
259
+ }
260
+ }), defineTool({
261
+ name: "quote_smt",
262
+ description: "Get a real-time SMT assembly price quote (single/double side, stencil, X-ray, conformal coating, Huaqiu agent part purchasing) for the requested region through Huaqiu/NextPCB. Use when the user asks about assembly costs, \"贴片价格\", \"SMT 报价\", \"焊接费用\", or delivery time for assembly. Embeds the PCB order (layers/size/quantity) used for the assembly quote. Returns the normalized price with SMT fee detail (no-tax, total, tax) and currency symbol. When derive_board_size / derive_board_stackup is set, the response carries the effective form: EDA-extracted board facts (panel size, embedded PCB length/width/layers/thickness) merged with the caller overrides — inspect \"form\" and re-query with corrected parameters instead of guessing. Quote-only — no order is created.",
263
+ parameters: {
264
+ region: {
265
+ type: "string",
266
+ enum: [
267
+ "cn",
268
+ "eu_us",
269
+ "jp"
270
+ ],
271
+ required: true,
272
+ description: "Pricing region. \"cn\" = Huaqiu China (eda.cn, ¥), \"eu_us\" = NextPCB EU/US, \"jp\" = NextPCB Japan (both ¥)."
273
+ },
274
+ number: {
275
+ type: "string",
276
+ description: "Assembly quantity as string (e.g. \"5\"). Default \"5\"."
277
+ },
278
+ buy_bom: {
279
+ type: "string",
280
+ enum: [
281
+ "hq_select",
282
+ "user_upload",
283
+ "online_match"
284
+ ],
285
+ description: "BOM parts source: hq_select = Huaqiu agent purchasing (default), user_upload = customer parts, online_match = online parts matching."
286
+ },
287
+ single_or_double_technique: {
288
+ type: "string",
289
+ enum: ["single", "double"],
290
+ description: "Single-side or double-side assembly. Default double."
291
+ },
292
+ pcb_blayer: {
293
+ type: "integer",
294
+ description: "Embedded PCB layer count, 1-64 (assembly quote depends on the PCB order). Default 4."
295
+ },
296
+ pcb_blength: {
297
+ type: "number",
298
+ description: "Embedded PCB board length in mm, > 0."
299
+ },
300
+ pcb_bwidth: {
301
+ type: "number",
302
+ description: "Embedded PCB board width in mm, > 0."
303
+ },
304
+ pcb_bcount: {
305
+ type: "integer",
306
+ description: "Embedded PCB panel quantity, >= 1. Default 5."
307
+ },
308
+ ...edaFlagProps
309
+ },
310
+ output: {
311
+ schema: { type: "json" },
312
+ render: renderJson
313
+ },
314
+ async execute(args, exec) {
315
+ const a = args;
316
+ const form = {};
317
+ if (a.number !== void 0) form.number = a.number;
318
+ if (a.buy_bom !== void 0) form.buyBom = a.buy_bom;
319
+ if (a.single_or_double_technique !== void 0) form.singleOrDoubleTechnique = a.single_or_double_technique;
320
+ const pcb = {};
321
+ if (a.pcb_blayer !== void 0) pcb.blayer = a.pcb_blayer;
322
+ if (a.pcb_blength !== void 0) pcb.blength = a.pcb_blength;
323
+ if (a.pcb_bwidth !== void 0) pcb.bwidth = a.pcb_bwidth;
324
+ if (a.pcb_bcount !== void 0) pcb.bcount = a.pcb_bcount;
325
+ if (Object.keys(pcb).length > 0) form.pcbOrder = pcb;
326
+ const payload = {
327
+ region: a.region,
328
+ form: Object.keys(form).length > 0 ? form : void 0,
329
+ eda: edaOf(a),
330
+ includeRawResponse: a.include_raw_response === true
331
+ };
332
+ return asJson(await client.quoteSmt(payload, { signal: exec.signal }));
333
+ }
334
+ })];
335
+ }
336
+ //#endregion
337
+ //#region src/index.ts
338
+ /** Plugin id — matches package.json. */
339
+ const name = "@huaqiu/dsh-tool-quote";
340
+ /** Cordis services this half depends on: the node tool registry + hq-edge
341
+ * bridge (REQUIRED — quote cannot work standalone). */
342
+ const inject = ["hqEdge", "tools"];
343
+ /** Shared component name for the unified DSH-plugin log. */
344
+ const COMPONENT = "dsh-tool-quote";
345
+ let _log = null;
346
+ function log() {
347
+ if (_log === null) _log = getLogger(COMPONENT);
348
+ return _log;
349
+ }
350
+ /**
351
+ * Host plugin body — register the two quote tools.
352
+ *
353
+ * The HQ Edge endpoint is resolved lazily at request time from the
354
+ * `ctx.hqEdge` service (edge-bridge), falling back to the overlay config /
355
+ * `HQ_EDGE_BASE_URL` env (same convention as `@huaqiu/dsh-eda-host`).
356
+ *
357
+ * @param ctx - real cordis context (node side).
358
+ * @returns disposer — unregisters the tools on plugin dispose.
359
+ */
360
+ function apply(ctx, config = {}) {
361
+ if (!ctx.tools || typeof ctx.tools.register !== "function") throw new Error("@huaqiu/dsh-tool-quote requires the DSH `tools` service (ctx.tools.register).");
362
+ const resolved = resolveQuoteToolConfig(config);
363
+ const hq = ctx.hqEdge;
364
+ if (!hq || typeof hq.baseUrl !== "string" || hq.baseUrl.trim().length === 0) throw new Error("@huaqiu/dsh-tool-quote requires a usable hq-edge context: the edge-bridge plugin did not provide ctx.hqEdge.baseUrl. Quote tools cannot work without hq-edge — check that the bridge started and that the HQ Edge port is valid.");
365
+ const getHqEdgeBaseUrl = () => {
366
+ const current = ctx.hqEdge;
367
+ return current?.baseUrl && current.baseUrl.trim().length > 0 ? current.baseUrl : void 0;
368
+ };
369
+ log().info("applying dsh-tool-quote node half", {
370
+ hasConfigHost: hasHost(resolved),
371
+ hqEdgeBaseUrlFromConfig: resolved.hqEdgeBaseUrl ?? null,
372
+ quotePathPrefix: resolved.quotePathPrefix,
373
+ requestTimeoutMs: resolved.requestTimeoutMs
374
+ });
375
+ const disposers = createQuoteTools({ client: createQuoteToolClient(resolved, { baseUrlResolver: getHqEdgeBaseUrl }) }).map((tool) => ctx.tools.register(tool));
376
+ log().info("dsh-tool-quote node half ready", { tools: disposers.length });
377
+ return () => {
378
+ for (const dispose of disposers) try {
379
+ dispose();
380
+ } catch {}
381
+ };
382
+ }
383
+ //#endregion
384
+ export { QuoteToolError, apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-tool-quote",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.5.3",
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.0"
23
+ },
24
+ "dependencies": {
25
+ "@huaqiu/dsh-plugin-log": "0.5.3"
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
+ }
6
40
  }
package/src/client.ts ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Transport for `@huaqiu/dsh-tool-quote`.
3
+ *
4
+ * DSH → dsh-tool-quote → hq-edge → price provider is the ONLY production
5
+ * path. This module POSTs the canonical quote request to the hq-edge quote
6
+ * router, maps the stable error envelope back to semantic kinds, and enforces
7
+ * the single end-to-end request budget. It contains no provider logic — that
8
+ * lives in hq-edge (apps/server/src/quote/*).
9
+ *
10
+ * hq-edge error envelope (stable contract):
11
+ *
12
+ * { error, detail, code?, status?, operation? }
13
+ *
14
+ * error → VALIDATION | UNIMPLEMENTED | UPSTREAM_HTTP | UPSTREAM_REJECTED
15
+ * | PARSE | UNAVAILABLE | INTERNAL
16
+ * status → 400 | 501 | 502 | 503 | 504 | 500
17
+ *
18
+ * @module
19
+ */
20
+
21
+ import type { QuoteToolConfig } from './config.js'
22
+ import { quoteUrlOf } from './config.js'
23
+
24
+ export type QuoteKind = 'pcb' | 'smt'
25
+
26
+ export interface QuoteToolClientDeps {
27
+ fetchImpl?: typeof fetch
28
+ /** Late-bound resolver for the HQ Edge base URL (edge-bridge service). */
29
+ baseUrlResolver?: () => string | undefined
30
+ }
31
+
32
+ export interface QuoteToolRequestOptions {
33
+ signal?: AbortSignal
34
+ }
35
+
36
+ /** Semantic failure of a quote request, mirroring hq-edge's envelope kinds. */
37
+ export class QuoteToolError extends Error {
38
+ constructor(
39
+ readonly kind: string,
40
+ message: string,
41
+ readonly status: number,
42
+ readonly detail?: string,
43
+ readonly code?: string | number,
44
+ ) {
45
+ super(message)
46
+ this.name = 'QuoteToolError'
47
+ }
48
+ }
49
+
50
+ /** HTTP status → semantic kind (hq-edge envelope, mirrors eda-host mapping). */
51
+ function statusToKind(status: number): string {
52
+ if (status === 400) return 'VALIDATION'
53
+ if (status === 501) return 'UNIMPLEMENTED'
54
+ if (status === 503) return 'UNAVAILABLE'
55
+ if (status === 504) return 'DEADLINE_EXCEEDED'
56
+ return 'INTERNAL'
57
+ }
58
+
59
+ function isAbortError(err: unknown): boolean {
60
+ const name = (err as Error | undefined)?.name
61
+ return name === 'AbortError' || name === 'TimeoutError'
62
+ }
63
+
64
+ function resolveSignal(deadlineMs: number, caller?: AbortSignal): AbortSignal | undefined {
65
+ const budget = deadlineMs > 0 ? AbortSignal.timeout(deadlineMs) : undefined
66
+ const signals = [caller, budget].filter((s): s is AbortSignal => Boolean(s))
67
+ if (signals.length === 0) return undefined
68
+ if (signals.length === 1) return signals[0]
69
+ const anyFn = (AbortSignal as unknown as { any?: (s: AbortSignal[]) => AbortSignal }).any
70
+ return typeof anyFn === 'function' ? anyFn.call(AbortSignal, signals) : signals[0]
71
+ }
72
+
73
+ export interface QuoteToolClient {
74
+ quotePcb(payload: unknown, options?: QuoteToolRequestOptions): Promise<unknown>
75
+ quoteSmt(payload: unknown, options?: QuoteToolRequestOptions): Promise<unknown>
76
+ }
77
+
78
+ export function createQuoteToolClient(
79
+ config: QuoteToolConfig,
80
+ deps: QuoteToolClientDeps = {},
81
+ ): QuoteToolClient {
82
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch
83
+
84
+ const resolveBaseUrl = (): string => {
85
+ const fromService = deps.baseUrlResolver?.()
86
+ const base = fromService?.trim().length ? fromService : config.hqEdgeBaseUrl
87
+ if (!base || base.trim().length === 0) {
88
+ throw new QuoteToolError(
89
+ 'FAILED_PRECONDITION',
90
+ 'quote: no hq-edge base URL available (edge-bridge service or HQ_EDGE_BASE_URL)',
91
+ 412,
92
+ )
93
+ }
94
+ return base.trim()
95
+ }
96
+
97
+ const request = async (
98
+ kind: QuoteKind,
99
+ payload: unknown,
100
+ options: QuoteToolRequestOptions = {},
101
+ ): Promise<unknown> => {
102
+ const base = resolveBaseUrl()
103
+ const url = quoteUrlOf({ ...config, hqEdgeBaseUrl: base }, kind)
104
+ const signal = resolveSignal(config.requestTimeoutMs ?? 0, options.signal)
105
+ try {
106
+ const response = await fetchImpl(url, {
107
+ method: 'POST',
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: JSON.stringify(payload),
110
+ signal,
111
+ })
112
+ const text = await response.text()
113
+ let body: unknown
114
+ try {
115
+ body = text.length > 0 ? JSON.parse(text) : null
116
+ } catch {
117
+ body = null
118
+ }
119
+ if (!response.ok) {
120
+ const envelope =
121
+ body && typeof body === 'object' && !Array.isArray(body)
122
+ ? (body as Record<string, unknown>)
123
+ : {}
124
+ const kindFromEnvelope =
125
+ typeof envelope.error === 'string' ? envelope.error : statusToKind(response.status)
126
+ throw new QuoteToolError(
127
+ kindFromEnvelope,
128
+ typeof envelope.detail === 'string' ? envelope.detail : 'quote request failed',
129
+ response.status,
130
+ typeof envelope.detail === 'string' ? envelope.detail : undefined,
131
+ typeof envelope.code === 'string' || typeof envelope.code === 'number'
132
+ ? envelope.code
133
+ : undefined,
134
+ )
135
+ }
136
+ return body
137
+ } catch (err) {
138
+ if (err instanceof QuoteToolError) throw err
139
+ if (isAbortError(err)) {
140
+ throw new QuoteToolError(
141
+ 'DEADLINE_EXCEEDED',
142
+ 'quote request timed out',
143
+ 504,
144
+ )
145
+ }
146
+ throw new QuoteToolError('INTERNAL', `quote request failed: ${String(err)}`, 500)
147
+ }
148
+ }
149
+
150
+ return {
151
+ quotePcb: (payload, options) => request('pcb', payload, options),
152
+ quoteSmt: (payload, options) => request('smt', payload, options),
153
+ }
154
+ }
package/src/config.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Runtime config resolution for `@huaqiu/dsh-tool-quote`.
3
+ *
4
+ * The plugin never opens a direct connection to the price providers — it only
5
+ * talks to `hq-edge`, which owns the quote capability (hq.fab.v1.QuoteService
6
+ * bridged over HTTP, see apps/server/src/routes/quote.ts on hq-edge). The base
7
+ * URL is delivered by the hq-edge supervisor as overlay config
8
+ * (`hqEdgeBaseUrl`), with `HQ_EDGE_BASE_URL` as env fallback — the same
9
+ * convention as `@huaqiu/dsh-eda-host` / `@huaqiu/dsh-auth`.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ export interface QuoteToolConfig {
15
+ /** HQ Edge base URL, e.g. "http://localhost:18080". Absent → no host. */
16
+ hqEdgeBaseUrl?: string
17
+ /** Path prefix on the host; default "/api/v1/quote". */
18
+ quotePathPrefix?: string
19
+ /** End-to-end budget for one quote request, in milliseconds (default 30_000). */
20
+ requestTimeoutMs?: number
21
+ }
22
+
23
+ export const DEFAULT_QUOTE_PATH_PREFIX = '/api/v1/quote'
24
+
25
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
26
+
27
+ export function resolveQuoteToolConfig(
28
+ config?: Partial<QuoteToolConfig> | null,
29
+ env: NodeJS.ProcessEnv = process.env,
30
+ ): QuoteToolConfig {
31
+ const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? ''
32
+ const pathPrefix =
33
+ config?.quotePathPrefix ?? env.HQ_EDGE_QUOTE_PATH ?? DEFAULT_QUOTE_PATH_PREFIX
34
+ const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS
35
+ const timeout = Number.parseInt(String(timeoutRaw ?? ''), 10)
36
+ return {
37
+ hqEdgeBaseUrl: baseUrl,
38
+ quotePathPrefix: pathPrefix,
39
+ requestTimeoutMs:
40
+ Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS,
41
+ }
42
+ }
43
+
44
+ /** True when a host base URL is available (host mode). */
45
+ export function hasHost(config: QuoteToolConfig): boolean {
46
+ return typeof config.hqEdgeBaseUrl === 'string' && config.hqEdgeBaseUrl.trim().length > 0
47
+ }
48
+
49
+ /** Build the absolute URL for one quote kind route ("pcb" | "smt"). */
50
+ export function quoteUrlOf(config: QuoteToolConfig, kind: 'pcb' | 'smt'): string {
51
+ const base = (config.hqEdgeBaseUrl ?? '').replace(/\/+$/, '')
52
+ const prefix = (config.quotePathPrefix ?? DEFAULT_QUOTE_PATH_PREFIX).replace(
53
+ /^\/+|\/+$/g,
54
+ '',
55
+ )
56
+ return `${base}/${prefix}/${kind}`
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Huaqiu EDA Quote DSH tool plugin (node half) — `@huaqiu/dsh-tool-quote`.
3
+ *
4
+ * Exposes real-time PCB / SMT price querying as two agent-visible tools:
5
+ *
6
+ * quote_pcb → hq-edge POST /api/v1/quote/pcb
7
+ * quote_smt → hq-edge POST /api/v1/quote/smt
8
+ *
9
+ * ── Architectural boundary ─────────────────────────────────────────────────
10
+ * Quote logic is owned ONCE by hq-edge (hq.fab.v1.QuoteService bridged over
11
+ * HTTP — apps/server/src/quote/*). This plugin is a thin adapter: it
12
+ * translates tool calls into canonical quote requests and returns the
13
+ * normalized QuotePrice. It contains NO provider logic (no eda.cn / nextpcb
14
+ * endpoints, no form serialization, no parsing) and no `@hqedge/*`
15
+ * dependency — it only talks to hq-edge through the edge-bridge.
16
+ *
17
+ * `hqEdge` is a REQUIRED inject: the plugin cannot reach hq-edge without the
18
+ * bridge's `ctx.hqEdge.baseUrl`. This matches the `@huaqiu/dsh-eda-host`
19
+ * contract (docs/tasks/expose-capability.md §3/§7) — quote tools are hq-edge
20
+ * capabilities, not standalone DSH features.
21
+ *
22
+ * @module @huaqiu/dsh-tool-quote
23
+ */
24
+
25
+ import type { Context } from '@deepseek-ai/cordis'
26
+ import { getLogger, type PluginLogger } from '@huaqiu/dsh-plugin-log'
27
+ import { createQuoteToolClient } from './client.js'
28
+ import {
29
+ hasHost,
30
+ resolveQuoteToolConfig,
31
+ type QuoteToolConfig,
32
+ } from './config.js'
33
+ import { createQuoteTools } from './tools.js'
34
+
35
+ /** Plugin id — matches package.json. */
36
+ export const name = '@huaqiu/dsh-tool-quote'
37
+
38
+ /** Cordis services this half depends on: the node tool registry + hq-edge
39
+ * bridge (REQUIRED — quote cannot work standalone). */
40
+ export const inject = ['hqEdge', 'tools'] as const
41
+
42
+ export type { QuoteToolConfig } from './config.js'
43
+ export { QuoteToolError } from './client.js'
44
+
45
+ declare module '@deepseek-ai/cordis' {
46
+ interface Context {
47
+ /**
48
+ * Provided by the hq-edge `edge-bridge` plugin (the HOST). `baseUrl` is
49
+ * the loopback HQ Edge endpoint, e.g. "http://localhost:18080". Read
50
+ * lazily on every request; REQUIRED for this plugin to load.
51
+ */
52
+ hqEdge: { baseUrl?: string }
53
+ }
54
+ }
55
+
56
+ /** Shared component name for the unified DSH-plugin log. */
57
+ const COMPONENT = 'dsh-tool-quote'
58
+
59
+ let _log: PluginLogger | null = null
60
+ function log(): PluginLogger {
61
+ if (_log === null) _log = getLogger(COMPONENT)
62
+ return _log
63
+ }
64
+
65
+ /**
66
+ * Host plugin body — register the two quote tools.
67
+ *
68
+ * The HQ Edge endpoint is resolved lazily at request time from the
69
+ * `ctx.hqEdge` service (edge-bridge), falling back to the overlay config /
70
+ * `HQ_EDGE_BASE_URL` env (same convention as `@huaqiu/dsh-eda-host`).
71
+ *
72
+ * @param ctx - real cordis context (node side).
73
+ * @returns disposer — unregisters the tools on plugin dispose.
74
+ */
75
+ export function apply(ctx: Context, config: Partial<QuoteToolConfig> = {}): () => void {
76
+ if (!ctx.tools || typeof ctx.tools.register !== 'function') {
77
+ throw new Error('@huaqiu/dsh-tool-quote requires the DSH `tools` service (ctx.tools.register).')
78
+ }
79
+
80
+ const resolved = resolveQuoteToolConfig(config)
81
+
82
+ const hq = ctx.hqEdge
83
+ if (!hq || typeof hq.baseUrl !== 'string' || hq.baseUrl.trim().length === 0) {
84
+ throw new Error(
85
+ '@huaqiu/dsh-tool-quote requires a usable hq-edge context: the edge-bridge ' +
86
+ 'plugin did not provide ctx.hqEdge.baseUrl. Quote tools cannot work without ' +
87
+ 'hq-edge — check that the bridge started and that the HQ Edge port is valid.',
88
+ )
89
+ }
90
+
91
+ const getHqEdgeBaseUrl = (): string | undefined => {
92
+ const current = ctx.hqEdge
93
+ return current?.baseUrl && current.baseUrl.trim().length > 0 ? current.baseUrl : undefined
94
+ }
95
+
96
+ log().info('applying dsh-tool-quote node half', {
97
+ hasConfigHost: hasHost(resolved),
98
+ hqEdgeBaseUrlFromConfig: resolved.hqEdgeBaseUrl ?? null,
99
+ quotePathPrefix: resolved.quotePathPrefix,
100
+ requestTimeoutMs: resolved.requestTimeoutMs,
101
+ })
102
+
103
+ const client = createQuoteToolClient(resolved, { baseUrlResolver: getHqEdgeBaseUrl })
104
+ const disposers = createQuoteTools({ client }).map((tool) => ctx.tools.register(tool))
105
+
106
+ log().info('dsh-tool-quote node half ready', { tools: disposers.length })
107
+
108
+ return () => {
109
+ for (const dispose of disposers) {
110
+ try {
111
+ dispose()
112
+ } catch {
113
+ // best-effort teardown
114
+ }
115
+ }
116
+ }
117
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Agent tools for `@huaqiu/dsh-tool-quote`.
3
+ *
4
+ * quote_pcb real-time PCB fabrication price for the requested region
5
+ * quote_smt real-time SMT assembly price (single-side / double-side)
6
+ *
7
+ * The tools are pure pass-throughs: they read validated arguments, build the
8
+ * canonical quote request (matching hq.fab.v1.QuoteService / the hq-edge
9
+ * router contract), call hq-edge via the client, and return the normalized
10
+ * QuotePrice as lossless JSON. Errors carry a semantic `kind`
11
+ * (FAILED_PRECONDITION / VALIDATION / UNIMPLEMENTED / UPSTREAM_REJECTED /
12
+ * DEADLINE_EXCEEDED / INTERNAL) — the agent sees why a quote failed, not a
13
+ * bare HTTP status.
14
+ *
15
+ * Note on enums: only the enumerated choices are accepted, mirroring the
16
+ * proto/type definitions on hq-edge. Unknown fields are rejected upstream
17
+ * (Zod .strict()), so the tools never send unsupported keys.
18
+ *
19
+ * @module
20
+ */
21
+
22
+ import { defineTool } from '@deepseek-ai/dsh-tools'
23
+ import type { QuoteToolClient } from './client.js'
24
+
25
+ /** Structural alias of the DSH `JsonValue`. */
26
+ type Json = string | number | boolean | null | Json[] | { [key: string]: Json }
27
+
28
+ function asJson<T>(value: T): Json {
29
+ return JSON.parse(JSON.stringify(value)) as Json
30
+ }
31
+
32
+ function renderJson(_args: unknown, value: unknown) {
33
+ return [{ type: 'text' as const, text: JSON.stringify(value) }]
34
+ }
35
+
36
+ /** Tool execution context (structural view of DSH's ToolRunContext). */
37
+ interface ToolExecLike {
38
+ signal?: AbortSignal
39
+ callId?: string
40
+ }
41
+
42
+ interface QuoteToolEnv {
43
+ client: QuoteToolClient
44
+ }
45
+
46
+ /** Pick the EDA derivation flags (only when explicitly true). */
47
+ function edaOf(args: Record<string, unknown>): Record<string, unknown> | undefined {
48
+ const eda: Record<string, unknown> = {}
49
+ if (args.derive_board_size === true) eda.deriveBoardSize = true
50
+ if (args.derive_board_stackup === true) eda.deriveBoardStackup = true
51
+ return Object.keys(eda).length > 0 ? eda : undefined
52
+ }
53
+
54
+ export function createQuoteTools(env: QuoteToolEnv) {
55
+ const { client } = env
56
+
57
+ /** Build the canonical PCB form from the flat tool args. */
58
+ const pcbFormOf = (a: Record<string, unknown>): Record<string, unknown> | undefined => {
59
+ const form: Record<string, unknown> = {}
60
+ if (a.bcount !== undefined) form.bcount = a.bcount
61
+ if (a.blayer !== undefined) form.blayer = a.blayer
62
+ if (a.blength !== undefined) form.blength = a.blength
63
+ if (a.bwidth !== undefined) form.bwidth = a.bwidth
64
+ if (a.board_tg !== undefined) form.boardTg = a.board_tg
65
+ if (a.color !== undefined) form.color = a.color
66
+ if (a.cover !== undefined) form.cover = a.cover
67
+ if (a.spray !== undefined) form.spray = a.spray
68
+ return Object.keys(form).length > 0 ? form : undefined
69
+ }
70
+
71
+ /** Optional EDA-host derivation flags shared by both tools. */
72
+ const edaFlagProps = {
73
+ derive_board_size: {
74
+ type: 'boolean' as const,
75
+ description:
76
+ 'When true, ask hq-edge to derive board length/width from the current PCB board ' +
77
+ '(approximate bounding box of footprints; only when a board is open in the host).',
78
+ },
79
+ derive_board_stackup: {
80
+ type: 'boolean' as const,
81
+ description:
82
+ 'NOT SUPPORTED (explicit capability gap): hq-edge has no board-properties source, ' +
83
+ 'so stack-up is never derived or fabricated. Provide blayer/bheight explicitly instead.',
84
+ },
85
+ include_raw_response: {
86
+ type: 'boolean' as const,
87
+ description: 'Include the bounded raw upstream payload for diagnostics (default false).',
88
+ },
89
+ }
90
+
91
+ return [
92
+ defineTool({
93
+ name: 'quote_pcb',
94
+ description:
95
+ 'Get a real-time PCB fabrication price quote (multi-layer, panel pricing, delivery options) ' +
96
+ 'for the requested region through Huaqiu/NextPCB. Use when the user asks how much a PCB ' +
97
+ 'would cost, "报价", "价格", delivery time estimates, or comparing fab options. ' +
98
+ 'Returns the itemized price breakdown, total price, and delivery alternatives ' +
99
+ '(name + build time in days + total price) with currency symbol (¥ CN / ¥ intl). ' +
100
+ 'When derive_board_size / derive_board_stackup is set, the response also carries the ' +
101
+ 'effective quote form: EDA-extracted board facts (length/width from the board outline, ' +
102
+ 'layer count + thickness from the stack-up in KiCad) merged with the caller overrides — ' +
103
+ 'plus server defaults applied on the wire (e.g. bcount default 5). Inspect "form" in the ' +
104
+ 'result, adjust any parameter the user wants to change (e.g. color, blayer, board_tg), and ' +
105
+ 're-query with the corrected form rather than guessing values. Progressive usage: quote_pcb ' +
106
+ 'for PCB fabrication, quote_smt for assembly, then the user places the order in the EDA ' +
107
+ 'host. This is a quote-only query — no order is created.',
108
+ parameters: {
109
+ region: {
110
+ type: 'string',
111
+ enum: ['cn', 'eu_us', 'jp'],
112
+ required: true,
113
+ description:
114
+ 'Pricing region. "cn" = Huaqiu China (eda.cn, ¥), "eu_us" = NextPCB EU/US, ' +
115
+ '"jp" = NextPCB Japan (both ¥).',
116
+ },
117
+ bcount: {
118
+ type: 'integer',
119
+ description: 'Panel quantity (boards per panel), >= 1. Default 5.',
120
+ },
121
+ blayer: {
122
+ type: 'integer',
123
+ description: 'PCB layer count, 1-64. Default 4.',
124
+ },
125
+ blength: {
126
+ type: 'number',
127
+ description: 'Board length in mm, > 0 (canonical mm; converted to cm on the wire).',
128
+ },
129
+ bwidth: {
130
+ type: 'number',
131
+ description: 'Board width in mm, > 0.',
132
+ },
133
+ board_tg: {
134
+ type: 'string',
135
+ enum: ['tg130', 'tg150', 'tg170'],
136
+ description: 'Glass transition temperature grade (4+ layers only).',
137
+ },
138
+ color: {
139
+ type: 'string',
140
+ enum: ['green', 'red', 'yellow', 'blue', 'white', 'black', 'matte_black'],
141
+ description: 'Solder mask color. Default green.',
142
+ },
143
+ cover: {
144
+ type: 'string',
145
+ enum: ['covered', 'exposed', 'solder_mask_plug', 'non_conductive_fill'],
146
+ description:
147
+ 'Via process (tenting / open / solder-mask-plug / non-conductive fill). Default covered.',
148
+ },
149
+ spray: {
150
+ type: 'string',
151
+ enum: ['hasl_lead', 'hasl_lf', 'enig', 'osp'],
152
+ description: 'Surface finish. Default HASL (lead).',
153
+ },
154
+ ...edaFlagProps,
155
+ },
156
+ output: { schema: { type: 'json' }, render: renderJson },
157
+ async execute(args: unknown, exec: ToolExecLike): Promise<Json> {
158
+ const a = args as Record<string, unknown>
159
+ const payload = {
160
+ region: a.region,
161
+ form: pcbFormOf(a),
162
+ eda: edaOf(a),
163
+ includeRawResponse: a.include_raw_response === true,
164
+ }
165
+ const result = await client.quotePcb(payload, { signal: exec.signal })
166
+ return asJson(result)
167
+ },
168
+ }),
169
+ defineTool({
170
+ name: 'quote_smt',
171
+ description:
172
+ 'Get a real-time SMT assembly price quote (single/double side, stencil, X-ray, conformal coating, ' +
173
+ 'Huaqiu agent part purchasing) for the requested region through Huaqiu/NextPCB. Use when the user ' +
174
+ 'asks about assembly costs, "贴片价格", "SMT 报价", "焊接费用", or delivery time for assembly. ' +
175
+ 'Embeds the PCB order (layers/size/quantity) used for the assembly quote. Returns the normalized ' +
176
+ 'price with SMT fee detail (no-tax, total, tax) and currency symbol. When derive_board_size / ' +
177
+ 'derive_board_stackup is set, the response carries the effective form: EDA-extracted board facts ' +
178
+ '(panel size, embedded PCB length/width/layers/thickness) merged with the caller overrides — ' +
179
+ 'inspect "form" and re-query with corrected parameters instead of guessing. Quote-only — no ' +
180
+ 'order is created.',
181
+ parameters: {
182
+ region: {
183
+ type: 'string',
184
+ enum: ['cn', 'eu_us', 'jp'],
185
+ required: true,
186
+ description:
187
+ 'Pricing region. "cn" = Huaqiu China (eda.cn, ¥), "eu_us" = NextPCB EU/US, ' +
188
+ '"jp" = NextPCB Japan (both ¥).',
189
+ },
190
+ number: {
191
+ type: 'string',
192
+ description: 'Assembly quantity as string (e.g. "5"). Default "5".',
193
+ },
194
+ buy_bom: {
195
+ type: 'string',
196
+ enum: ['hq_select', 'user_upload', 'online_match'],
197
+ description:
198
+ 'BOM parts source: hq_select = Huaqiu agent purchasing (default), ' +
199
+ 'user_upload = customer parts, online_match = online parts matching.',
200
+ },
201
+ single_or_double_technique: {
202
+ type: 'string',
203
+ enum: ['single', 'double'],
204
+ description: 'Single-side or double-side assembly. Default double.',
205
+ },
206
+ pcb_blayer: {
207
+ type: 'integer',
208
+ description:
209
+ 'Embedded PCB layer count, 1-64 (assembly quote depends on the PCB order). Default 4.',
210
+ },
211
+ pcb_blength: {
212
+ type: 'number',
213
+ description: 'Embedded PCB board length in mm, > 0.',
214
+ },
215
+ pcb_bwidth: {
216
+ type: 'number',
217
+ description: 'Embedded PCB board width in mm, > 0.',
218
+ },
219
+ pcb_bcount: {
220
+ type: 'integer',
221
+ description: 'Embedded PCB panel quantity, >= 1. Default 5.',
222
+ },
223
+ ...edaFlagProps,
224
+ },
225
+ output: { schema: { type: 'json' }, render: renderJson },
226
+ async execute(args: unknown, exec: ToolExecLike): Promise<Json> {
227
+ const a = args as Record<string, unknown>
228
+ const form: Record<string, unknown> = {}
229
+ if (a.number !== undefined) form.number = a.number
230
+ if (a.buy_bom !== undefined) form.buyBom = a.buy_bom
231
+ if (a.single_or_double_technique !== undefined)
232
+ form.singleOrDoubleTechnique = a.single_or_double_technique
233
+ const pcb: Record<string, unknown> = {}
234
+ if (a.pcb_blayer !== undefined) pcb.blayer = a.pcb_blayer
235
+ if (a.pcb_blength !== undefined) pcb.blength = a.pcb_blength
236
+ if (a.pcb_bwidth !== undefined) pcb.bwidth = a.pcb_bwidth
237
+ if (a.pcb_bcount !== undefined) pcb.bcount = a.pcb_bcount
238
+ if (Object.keys(pcb).length > 0) form.pcbOrder = pcb
239
+ const payload = {
240
+ region: a.region,
241
+ form: Object.keys(form).length > 0 ? form : undefined,
242
+ eda: edaOf(a),
243
+ includeRawResponse: a.include_raw_response === true,
244
+ }
245
+ const result = await client.quoteSmt(payload, { signal: exec.signal })
246
+ return asJson(result)
247
+ },
248
+ }),
249
+ ]
250
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.