@huaqiu/dsh-tool-quote 0.5.4 → 0.5.5

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/lib/index.mjs CHANGED
@@ -20,6 +20,10 @@ function hasHost(config) {
20
20
  function quoteUrlOf(config, kind) {
21
21
  return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.quotePathPrefix ?? "/api/v1/quote").replace(/^\/+|\/+$/g, "")}/${kind}`;
22
22
  }
23
+ /** Build the absolute URL for a place-order trigger route ("pcb" | "smt"). */
24
+ function placeOrderUrlOf(config, kind) {
25
+ return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.quotePathPrefix ?? "/api/v1/quote").replace(/^\/+|\/+$/g, "")}/place/${kind}`;
26
+ }
23
27
  //#endregion
24
28
  //#region src/client.ts
25
29
  /** Semantic failure of a quote request, mirroring hq-edge's envelope kinds. */
@@ -96,9 +100,43 @@ function createQuoteToolClient(config, deps = {}) {
96
100
  throw new QuoteToolError("INTERNAL", `quote request failed: ${String(err)}`, 500);
97
101
  }
98
102
  };
103
+ const place = async (kind, payload, options = {}) => {
104
+ const base = resolveBaseUrl();
105
+ const url = placeOrderUrlOf({
106
+ ...config,
107
+ hqEdgeBaseUrl: base
108
+ }, kind);
109
+ const signal = resolveSignal(config.requestTimeoutMs ?? 0, options.signal);
110
+ try {
111
+ const response = await fetchImpl(url, {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify(payload),
115
+ signal
116
+ });
117
+ const text = await response.text();
118
+ let body;
119
+ try {
120
+ body = text.length > 0 ? JSON.parse(text) : null;
121
+ } catch {
122
+ body = null;
123
+ }
124
+ if (!response.ok) {
125
+ const envelope = body && typeof body === "object" && !Array.isArray(body) ? body : {};
126
+ const rawKind = typeof envelope.error === "string" ? envelope.error : statusToKind(response.status);
127
+ throw new QuoteToolError(rawKind === "EDA_HOST_NOT_READY" ? "FAILED_PRECONDITION" : rawKind, typeof envelope.detail === "string" ? envelope.detail : "place order request failed", response.status, typeof envelope.detail === "string" ? envelope.detail : void 0, typeof envelope.code === "string" || typeof envelope.code === "number" ? envelope.code : void 0);
128
+ }
129
+ return body;
130
+ } catch (err) {
131
+ if (err instanceof QuoteToolError) throw err;
132
+ if (isAbortError(err)) throw new QuoteToolError("DEADLINE_EXCEEDED", "place order request timed out", 504);
133
+ throw new QuoteToolError("INTERNAL", `place order request failed: ${String(err)}`, 500);
134
+ }
135
+ };
99
136
  return {
100
137
  quotePcb: (payload, options) => request("pcb", payload, options),
101
- quoteSmt: (payload, options) => request("smt", payload, options)
138
+ quoteSmt: (payload, options) => request("smt", payload, options),
139
+ placeOrder: (kind, payload, options) => place(kind, payload, options)
102
140
  };
103
141
  }
104
142
  //#endregion
@@ -169,167 +207,212 @@ function createQuoteTools(env) {
169
207
  description: "Include the bounded raw upstream payload for diagnostics (default false)."
170
208
  }
171
209
  };
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, endpoint-required pbnum=1 / insidecopper). 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. All regions are anonymous-queryable (no login required). The pricing region is auto-detected by hq-edge from the trusted request context (CN / JP / INTERNATIONAL) — do NOT force a region unless the user explicitly asks for one.",
175
- parameters: {
176
- region: {
177
- type: "string",
178
- enum: [
179
- "cn",
180
- "eu_us",
181
- "jp"
182
- ],
183
- description: "Optional explicit override. When omitted, hq-edge auto-detects the pricing region from the trusted request context (CN / JP / INTERNATIONAL). \"cn\" = Huaqiu China (eda.cn, ¥), \"eu_us\" = NextPCB EU/US, \"jp\" = NextPCB Japan (both ¥)."
184
- },
185
- bcount: {
186
- type: "integer",
187
- description: "Panel quantity (boards per panel), >= 1. Default 5."
188
- },
189
- blayer: {
190
- type: "integer",
191
- description: "PCB layer count, 1-64. Default 4."
210
+ return [
211
+ defineTool({
212
+ name: "quote_pcb",
213
+ 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, endpoint-required pbnum=1 / insidecopper). 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. All regions are anonymous-queryable (no login required). The pricing region is auto-detected by hq-edge from the trusted request context (CN / JP / INTERNATIONAL) — do NOT force a region unless the user explicitly asks for one.",
214
+ parameters: {
215
+ region: {
216
+ type: "string",
217
+ enum: [
218
+ "cn",
219
+ "eu_us",
220
+ "jp"
221
+ ],
222
+ description: "Optional explicit override. When omitted, hq-edge auto-detects the pricing region from the trusted request context (CN / JP / INTERNATIONAL). \"cn\" = Huaqiu China (eda.cn, ¥), \"eu_us\" = NextPCB EU/US, \"jp\" = NextPCB Japan (both ¥)."
223
+ },
224
+ bcount: {
225
+ type: "integer",
226
+ description: "Panel quantity (boards per panel), >= 1. Default 5."
227
+ },
228
+ blayer: {
229
+ type: "integer",
230
+ description: "PCB layer count, 1-64. Default 4."
231
+ },
232
+ blength: {
233
+ type: "number",
234
+ description: "Board length in mm, > 0 (canonical mm; converted to cm on the wire)."
235
+ },
236
+ bwidth: {
237
+ type: "number",
238
+ description: "Board width in mm, > 0."
239
+ },
240
+ board_tg: {
241
+ type: "string",
242
+ enum: [
243
+ "tg130",
244
+ "tg150",
245
+ "tg170"
246
+ ],
247
+ description: "Glass transition temperature grade (4+ layers only)."
248
+ },
249
+ color: {
250
+ type: "string",
251
+ enum: [
252
+ "green",
253
+ "red",
254
+ "yellow",
255
+ "blue",
256
+ "white",
257
+ "black",
258
+ "matte_black"
259
+ ],
260
+ description: "Solder mask color. Default green."
261
+ },
262
+ cover: {
263
+ type: "string",
264
+ enum: [
265
+ "covered",
266
+ "exposed",
267
+ "solder_mask_plug",
268
+ "non_conductive_fill"
269
+ ],
270
+ description: "Via process (tenting / open / solder-mask-plug / non-conductive fill). Default covered."
271
+ },
272
+ spray: {
273
+ type: "string",
274
+ enum: [
275
+ "hasl_lead",
276
+ "hasl_lf",
277
+ "enig",
278
+ "osp"
279
+ ],
280
+ description: "Surface finish. Default HASL (lead)."
281
+ },
282
+ ...edaFlagProps
192
283
  },
193
- blength: {
194
- type: "number",
195
- description: "Board length in mm, > 0 (canonical mm; converted to cm on the wire)."
284
+ output: {
285
+ schema: { type: "json" },
286
+ render: renderJson
196
287
  },
197
- bwidth: {
198
- type: "number",
199
- description: "Board width in mm, > 0."
200
- },
201
- board_tg: {
202
- type: "string",
203
- enum: [
204
- "tg130",
205
- "tg150",
206
- "tg170"
207
- ],
208
- description: "Glass transition temperature grade (4+ layers only)."
209
- },
210
- color: {
211
- type: "string",
212
- enum: [
213
- "green",
214
- "red",
215
- "yellow",
216
- "blue",
217
- "white",
218
- "black",
219
- "matte_black"
220
- ],
221
- description: "Solder mask color. Default green."
222
- },
223
- cover: {
224
- type: "string",
225
- enum: [
226
- "covered",
227
- "exposed",
228
- "solder_mask_plug",
229
- "non_conductive_fill"
230
- ],
231
- description: "Via process (tenting / open / solder-mask-plug / non-conductive fill). Default covered."
288
+ async execute(args, exec) {
289
+ const a = args;
290
+ const payload = {
291
+ region: a.region,
292
+ form: pcbFormOf(a),
293
+ eda: edaOf(a),
294
+ includeRawResponse: a.include_raw_response === true
295
+ };
296
+ return asJson(await client.quotePcb(payload, { signal: exec.signal }));
297
+ }
298
+ }),
299
+ defineTool({
300
+ name: "quote_smt",
301
+ 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. The pricing region is auto-detected by hq-edge from the trusted request context (CN / JP / INTERNATIONAL) — do NOT force a region unless the user explicitly asks.",
302
+ parameters: {
303
+ region: {
304
+ type: "string",
305
+ enum: [
306
+ "cn",
307
+ "eu_us",
308
+ "jp"
309
+ ],
310
+ description: "Optional explicit override. When omitted, hq-edge auto-detects the pricing region from the trusted request context (CN / JP / INTERNATIONAL). \"cn\" = Huaqiu China (eda.cn, ¥), \"eu_us\" = NextPCB EU/US, \"jp\" = NextPCB Japan (both ¥)."
311
+ },
312
+ number: {
313
+ type: "string",
314
+ description: "Assembly quantity as string (e.g. \"5\"). Default \"5\"."
315
+ },
316
+ buy_bom: {
317
+ type: "string",
318
+ enum: [
319
+ "hq_select",
320
+ "user_upload",
321
+ "online_match"
322
+ ],
323
+ description: "BOM parts source: hq_select = Huaqiu agent purchasing (default), user_upload = customer parts, online_match = online parts matching."
324
+ },
325
+ single_or_double_technique: {
326
+ type: "string",
327
+ enum: ["single", "double"],
328
+ description: "Single-side or double-side assembly. Default double."
329
+ },
330
+ pcb_blayer: {
331
+ type: "integer",
332
+ description: "Embedded PCB layer count, 1-64 (assembly quote depends on the PCB order). Default 4."
333
+ },
334
+ pcb_blength: {
335
+ type: "number",
336
+ description: "Embedded PCB board length in mm, > 0."
337
+ },
338
+ pcb_bwidth: {
339
+ type: "number",
340
+ description: "Embedded PCB board width in mm, > 0."
341
+ },
342
+ pcb_bcount: {
343
+ type: "integer",
344
+ description: "Embedded PCB panel quantity, >= 1. Default 5. NOTE: the embedded board size (pcb_blength/pcb_bwidth) is also used as the assembly panel size (panel = single board) when no explicit panel size is given — the Intl SMT endpoint rejects the request otherwise (\"pcb_width can not empty\", response_code 1003)."
345
+ },
346
+ ...edaFlagProps
232
347
  },
233
- spray: {
234
- type: "string",
235
- enum: [
236
- "hasl_lead",
237
- "hasl_lf",
238
- "enig",
239
- "osp"
240
- ],
241
- description: "Surface finish. Default HASL (lead)."
348
+ output: {
349
+ schema: { type: "json" },
350
+ render: renderJson
242
351
  },
243
- ...edaFlagProps
244
- },
245
- output: {
246
- schema: { type: "json" },
247
- render: renderJson
248
- },
249
- async execute(args, exec) {
250
- const a = args;
251
- const payload = {
252
- region: a.region,
253
- form: pcbFormOf(a),
254
- eda: edaOf(a),
255
- includeRawResponse: a.include_raw_response === true
256
- };
257
- return asJson(await client.quotePcb(payload, { signal: exec.signal }));
258
- }
259
- }), defineTool({
260
- name: "quote_smt",
261
- 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. The pricing region is auto-detected by hq-edge from the trusted request context (CN / JP / INTERNATIONAL) — do NOT force a region unless the user explicitly asks.",
262
- parameters: {
263
- region: {
352
+ async execute(args, exec) {
353
+ const a = args;
354
+ const form = {};
355
+ if (a.number !== void 0) form.number = a.number;
356
+ if (a.buy_bom !== void 0) form.buyBom = a.buy_bom;
357
+ if (a.single_or_double_technique !== void 0) form.singleOrDoubleTechnique = a.single_or_double_technique;
358
+ const pcb = {};
359
+ if (a.pcb_blayer !== void 0) pcb.blayer = a.pcb_blayer;
360
+ if (a.pcb_blength !== void 0) pcb.blength = a.pcb_blength;
361
+ if (a.pcb_bwidth !== void 0) pcb.bwidth = a.pcb_bwidth;
362
+ if (a.pcb_bcount !== void 0) pcb.bcount = a.pcb_bcount;
363
+ if (Object.keys(pcb).length > 0) form.pcbOrder = pcb;
364
+ const payload = {
365
+ region: a.region,
366
+ form: Object.keys(form).length > 0 ? form : void 0,
367
+ eda: edaOf(a),
368
+ includeRawResponse: a.include_raw_response === true
369
+ };
370
+ return asJson(await client.quoteSmt(payload, { signal: exec.signal }));
371
+ }
372
+ }),
373
+ defineTool({
374
+ name: "place_pcb_order",
375
+ description: "Trigger the EDA host's PCB place-order workflow (KiCad ACTIONS::placeOrderForPCB). The host validates the Huaqiu login (opens its login dialog first if needed), prepares the order artifacts (gerber/drill/bom/zip) from the currently open PCB board, uploads them and opens the place-order page in the editor's own dialog. Use when the user asks to \"下单\", \"place order\", \"提交打样/PCB订单\", or after a quote_pcb has been reviewed and the user wants to actually order. Returns { orderUrl, status, message } — status \"launched\" means the order dialog was opened in the editor; the user completes the order there. NOT a quote — this creates/enters the order flow. Preconditions: a PCB editor with a board open in the EDA host (otherwise FAILED_PRECONDITION), and a logged-in Huaqiu account (the host prompts login itself). The region is auto-detected by hq-edge from the trusted request context — do NOT force a region unless the user explicitly asks.",
376
+ parameters: { region: {
264
377
  type: "string",
265
378
  enum: [
266
379
  "cn",
267
380
  "eu_us",
268
381
  "jp"
269
382
  ],
270
- description: "Optional explicit override. When omitted, hq-edge auto-detects the pricing region from the trusted request context (CN / JP / INTERNATIONAL). \"cn\" = Huaqiu China (eda.cn, ¥), \"eu_us\" = NextPCB EU/US, \"jp\" = NextPCB Japan (both ¥)."
383
+ description: "Optional explicit override. When omitted, hq-edge auto-detects the pricing region from the trusted request context (CN / JP / INTERNATIONAL). \"cn\" = Huaqiu China (eda.cn), \"eu_us\" = NextPCB EU/US, \"jp\" = NextPCB Japan."
384
+ } },
385
+ output: {
386
+ schema: { type: "json" },
387
+ render: renderJson
271
388
  },
272
- number: {
273
- type: "string",
274
- description: "Assembly quantity as string (e.g. \"5\"). Default \"5\"."
275
- },
276
- buy_bom: {
389
+ async execute(args, exec) {
390
+ const payload = { region: args.region };
391
+ return asJson(await client.placeOrder("pcb", payload, { signal: exec.signal }));
392
+ }
393
+ }),
394
+ defineTool({
395
+ name: "place_smt_order",
396
+ description: "Trigger the EDA host's SMT assembly place-order workflow (KiCad ACTIONS::placeOrderForSMT). Same flow as place_pcb_order but for assembly: the host validates login, prepares the assembly order artifacts (board zip + BOM + component positions) from the currently open PCB, uploads them and opens the place-order page in the editor's own dialog. Use when the user wants to \"贴片下单\", \"SMT 下单\", \"place SMT order\", or after quote_smt has been reviewed. Returns { orderUrl, status, message }. Preconditions: a PCB editor with a board open in the EDA host, and a logged-in Huaqiu account (the host prompts login itself). The region is auto-detected by hq-edge — do NOT force a region unless the user explicitly asks.",
397
+ parameters: { region: {
277
398
  type: "string",
278
399
  enum: [
279
- "hq_select",
280
- "user_upload",
281
- "online_match"
400
+ "cn",
401
+ "eu_us",
402
+ "jp"
282
403
  ],
283
- description: "BOM parts source: hq_select = Huaqiu agent purchasing (default), user_upload = customer parts, online_match = online parts matching."
284
- },
285
- single_or_double_technique: {
286
- type: "string",
287
- enum: ["single", "double"],
288
- description: "Single-side or double-side assembly. Default double."
289
- },
290
- pcb_blayer: {
291
- type: "integer",
292
- description: "Embedded PCB layer count, 1-64 (assembly quote depends on the PCB order). Default 4."
293
- },
294
- pcb_blength: {
295
- type: "number",
296
- description: "Embedded PCB board length in mm, > 0."
297
- },
298
- pcb_bwidth: {
299
- type: "number",
300
- description: "Embedded PCB board width in mm, > 0."
301
- },
302
- pcb_bcount: {
303
- type: "integer",
304
- description: "Embedded PCB panel quantity, >= 1. Default 5. NOTE: the embedded board size (pcb_blength/pcb_bwidth) is also used as the assembly panel size (panel = single board) when no explicit panel size is given — the Intl SMT endpoint rejects the request otherwise (\"pcb_width can not empty\", response_code 1003)."
404
+ description: "Optional explicit override. When omitted, hq-edge auto-detects the pricing region from the trusted request context (CN / JP / INTERNATIONAL). \"cn\" = Huaqiu China (eda.cn), \"eu_us\" = NextPCB EU/US, \"jp\" = NextPCB Japan."
405
+ } },
406
+ output: {
407
+ schema: { type: "json" },
408
+ render: renderJson
305
409
  },
306
- ...edaFlagProps
307
- },
308
- output: {
309
- schema: { type: "json" },
310
- render: renderJson
311
- },
312
- async execute(args, exec) {
313
- const a = args;
314
- const form = {};
315
- if (a.number !== void 0) form.number = a.number;
316
- if (a.buy_bom !== void 0) form.buyBom = a.buy_bom;
317
- if (a.single_or_double_technique !== void 0) form.singleOrDoubleTechnique = a.single_or_double_technique;
318
- const pcb = {};
319
- if (a.pcb_blayer !== void 0) pcb.blayer = a.pcb_blayer;
320
- if (a.pcb_blength !== void 0) pcb.blength = a.pcb_blength;
321
- if (a.pcb_bwidth !== void 0) pcb.bwidth = a.pcb_bwidth;
322
- if (a.pcb_bcount !== void 0) pcb.bcount = a.pcb_bcount;
323
- if (Object.keys(pcb).length > 0) form.pcbOrder = pcb;
324
- const payload = {
325
- region: a.region,
326
- form: Object.keys(form).length > 0 ? form : void 0,
327
- eda: edaOf(a),
328
- includeRawResponse: a.include_raw_response === true
329
- };
330
- return asJson(await client.quoteSmt(payload, { signal: exec.signal }));
331
- }
332
- })];
410
+ async execute(args, exec) {
411
+ const payload = { region: args.region };
412
+ return asJson(await client.placeOrder("smt", payload, { signal: exec.signal }));
413
+ }
414
+ })
415
+ ];
333
416
  }
334
417
  //#endregion
335
418
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-tool-quote",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "type": "module",
5
5
  "main": "./lib/index.mjs",
6
6
  "types": "./lib/index.d.mts",
@@ -22,7 +22,7 @@
22
22
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.0"
23
23
  },
24
24
  "dependencies": {
25
- "@huaqiu/dsh-plugin-log": "0.5.4"
25
+ "@huaqiu/dsh-plugin-log": "0.5.5"
26
26
  },
27
27
  "files": [
28
28
  "lib",
package/src/client.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  */
20
20
 
21
21
  import type { QuoteToolConfig } from './config.js'
22
- import { quoteUrlOf } from './config.js'
22
+ import { quoteUrlOf, placeOrderUrlOf } from './config.js'
23
23
 
24
24
  export type QuoteKind = 'pcb' | 'smt'
25
25
 
@@ -73,6 +73,12 @@ function resolveSignal(deadlineMs: number, caller?: AbortSignal): AbortSignal |
73
73
  export interface QuoteToolClient {
74
74
  quotePcb(payload: unknown, options?: QuoteToolRequestOptions): Promise<unknown>
75
75
  quoteSmt(payload: unknown, options?: QuoteToolRequestOptions): Promise<unknown>
76
+ /**
77
+ * Trigger the EDA host's place-order workflow (hq-edge
78
+ * POST /api/v1/quote/place/:kind → QuoteService.PlacePcbOrder / PlaceSmtOrder
79
+ * → KiCad ACTIONS::placeOrderForPCB / placeOrderForSMT). `kind` is 'pcb' | 'smt'.
80
+ */
81
+ placeOrder(kind: 'pcb' | 'smt', payload: unknown, options?: QuoteToolRequestOptions): Promise<unknown>
76
82
  }
77
83
 
78
84
  export function createQuoteToolClient(
@@ -147,8 +153,67 @@ export function createQuoteToolClient(
147
153
  }
148
154
  }
149
155
 
156
+ const place = async (
157
+ kind: 'pcb' | 'smt',
158
+ payload: unknown,
159
+ options: QuoteToolRequestOptions = {},
160
+ ): Promise<unknown> => {
161
+ const base = resolveBaseUrl()
162
+ const url = placeOrderUrlOf({ ...config, hqEdgeBaseUrl: base }, kind)
163
+ const signal = resolveSignal(config.requestTimeoutMs ?? 0, options.signal)
164
+ try {
165
+ const response = await fetchImpl(url, {
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/json' },
168
+ body: JSON.stringify(payload),
169
+ signal,
170
+ })
171
+ const text = await response.text()
172
+ let body: unknown
173
+ try {
174
+ body = text.length > 0 ? JSON.parse(text) : null
175
+ } catch {
176
+ body = null
177
+ }
178
+ if (!response.ok) {
179
+ const envelope =
180
+ body && typeof body === 'object' && !Array.isArray(body)
181
+ ? (body as Record<string, unknown>)
182
+ : {}
183
+ // Normalize the host envelope into the semantic kinds DSH agents
184
+ // branch on: EDA_HOST_NOT_READY (no PCB editor / not ready) maps to
185
+ // FAILED_PRECONDITION, anything else keeps its host kind.
186
+ const rawKind =
187
+ typeof envelope.error === 'string' ? envelope.error : statusToKind(response.status)
188
+ const kindFromEnvelope =
189
+ rawKind === 'EDA_HOST_NOT_READY' ? 'FAILED_PRECONDITION' : rawKind
190
+ throw new QuoteToolError(
191
+ kindFromEnvelope,
192
+ typeof envelope.detail === 'string' ? envelope.detail : 'place order request failed',
193
+ response.status,
194
+ typeof envelope.detail === 'string' ? envelope.detail : undefined,
195
+ typeof envelope.code === 'string' || typeof envelope.code === 'number'
196
+ ? envelope.code
197
+ : undefined,
198
+ )
199
+ }
200
+ return body
201
+ } catch (err) {
202
+ if (err instanceof QuoteToolError) throw err
203
+ if (isAbortError(err)) {
204
+ throw new QuoteToolError(
205
+ 'DEADLINE_EXCEEDED',
206
+ 'place order request timed out',
207
+ 504,
208
+ )
209
+ }
210
+ throw new QuoteToolError('INTERNAL', `place order request failed: ${String(err)}`, 500)
211
+ }
212
+ }
213
+
150
214
  return {
151
215
  quotePcb: (payload, options) => request('pcb', payload, options),
152
216
  quoteSmt: (payload, options) => request('smt', payload, options),
217
+ placeOrder: (kind, payload, options) => place(kind, payload, options),
153
218
  }
154
219
  }
package/src/config.ts CHANGED
@@ -55,3 +55,13 @@ export function quoteUrlOf(config: QuoteToolConfig, kind: 'pcb' | 'smt'): string
55
55
  )
56
56
  return `${base}/${prefix}/${kind}`
57
57
  }
58
+
59
+ /** Build the absolute URL for a place-order trigger route ("pcb" | "smt"). */
60
+ export function placeOrderUrlOf(config: QuoteToolConfig, kind: 'pcb' | 'smt'): string {
61
+ const base = (config.hqEdgeBaseUrl ?? '').replace(/\/+$/, '')
62
+ const prefix = (config.quotePathPrefix ?? DEFAULT_QUOTE_PATH_PREFIX).replace(
63
+ /^\/+|\/+$/g,
64
+ '',
65
+ )
66
+ return `${base}/${prefix}/place/${kind}`
67
+ }
package/src/tools.ts CHANGED
@@ -253,5 +253,66 @@ export function createQuoteTools(env: QuoteToolEnv) {
253
253
  return asJson(result)
254
254
  },
255
255
  }),
256
+ defineTool({
257
+ name: 'place_pcb_order',
258
+ description:
259
+ 'Trigger the EDA host\'s PCB place-order workflow (KiCad ACTIONS::placeOrderForPCB). ' +
260
+ 'The host validates the Huaqiu login (opens its login dialog first if needed), prepares ' +
261
+ 'the order artifacts (gerber/drill/bom/zip) from the currently open PCB board, uploads ' +
262
+ 'them and opens the place-order page in the editor\'s own dialog. Use when the user asks ' +
263
+ 'to "下单", "place order", "提交打样/PCB订单", or after a quote_pcb has been reviewed and the ' +
264
+ 'user wants to actually order. Returns { orderUrl, status, message } — status "launched" ' +
265
+ 'means the order dialog was opened in the editor; the user completes the order there. ' +
266
+ 'NOT a quote — this creates/enters the order flow. Preconditions: a PCB editor with a ' +
267
+ 'board open in the EDA host (otherwise FAILED_PRECONDITION), and a logged-in Huaqiu ' +
268
+ 'account (the host prompts login itself). The region is auto-detected by hq-edge from the ' +
269
+ 'trusted request context — do NOT force a region unless the user explicitly asks.',
270
+ parameters: {
271
+ region: {
272
+ type: 'string',
273
+ enum: ['cn', 'eu_us', 'jp'],
274
+ description:
275
+ 'Optional explicit override. When omitted, hq-edge auto-detects the pricing region from ' +
276
+ 'the trusted request context (CN / JP / INTERNATIONAL). "cn" = Huaqiu China (eda.cn), ' +
277
+ '"eu_us" = NextPCB EU/US, "jp" = NextPCB Japan.',
278
+ },
279
+ },
280
+ output: { schema: { type: 'json' }, render: renderJson },
281
+ async execute(args: unknown, exec: ToolExecLike): Promise<Json> {
282
+ const a = args as Record<string, unknown>
283
+ const payload = { region: a.region }
284
+ const result = await client.placeOrder('pcb', payload, { signal: exec.signal })
285
+ return asJson(result)
286
+ },
287
+ }),
288
+ defineTool({
289
+ name: 'place_smt_order',
290
+ description:
291
+ 'Trigger the EDA host\'s SMT assembly place-order workflow (KiCad ACTIONS::placeOrderForSMT). ' +
292
+ 'Same flow as place_pcb_order but for assembly: the host validates login, prepares the ' +
293
+ 'assembly order artifacts (board zip + BOM + component positions) from the currently open ' +
294
+ 'PCB, uploads them and opens the place-order page in the editor\'s own dialog. Use when the ' +
295
+ 'user wants to "贴片下单", "SMT 下单", "place SMT order", or after quote_smt has been reviewed. ' +
296
+ 'Returns { orderUrl, status, message }. Preconditions: a PCB editor with a board open in the ' +
297
+ 'EDA host, and a logged-in Huaqiu account (the host prompts login itself). The region is ' +
298
+ 'auto-detected by hq-edge — do NOT force a region unless the user explicitly asks.',
299
+ parameters: {
300
+ region: {
301
+ type: 'string',
302
+ enum: ['cn', 'eu_us', 'jp'],
303
+ description:
304
+ 'Optional explicit override. When omitted, hq-edge auto-detects the pricing region from ' +
305
+ 'the trusted request context (CN / JP / INTERNATIONAL). "cn" = Huaqiu China (eda.cn), ' +
306
+ '"eu_us" = NextPCB EU/US, "jp" = NextPCB Japan.',
307
+ },
308
+ },
309
+ output: { schema: { type: 'json' }, render: renderJson },
310
+ async execute(args: unknown, exec: ToolExecLike): Promise<Json> {
311
+ const a = args as Record<string, unknown>
312
+ const payload = { region: a.region }
313
+ const result = await client.placeOrder('smt', payload, { signal: exec.signal })
314
+ return asJson(result)
315
+ },
316
+ }),
256
317
  ]
257
318
  }