@huaqiu/dsh-eda-host 0.3.25 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.mts +126 -1
- package/lib/index.mjs +95 -43
- package/package.json +2 -2
- package/src/client.ts +41 -1
- package/src/config.ts +19 -0
- package/src/index.ts +16 -1
- package/src/tools.ts +35 -1
- package/src/types.ts +132 -0
package/lib/index.d.mts
CHANGED
|
@@ -16,6 +16,8 @@ interface EdaHostConfig {
|
|
|
16
16
|
hqEdgeBaseUrl?: string;
|
|
17
17
|
/** Path prefix on the host; default "/api/v1/netlist". */
|
|
18
18
|
netlistPathPrefix?: string;
|
|
19
|
+
/** Path prefix for PCB selection; default "/api/v1/pcb-selection". */
|
|
20
|
+
pcbSelectionPathPrefix?: string;
|
|
19
21
|
/** Path prefix for host discovery; default "/api/v1/host". */
|
|
20
22
|
hostPathPrefix?: string;
|
|
21
23
|
/**
|
|
@@ -68,6 +70,123 @@ interface SchematicNetlist {
|
|
|
68
70
|
components: SchematicComponent[];
|
|
69
71
|
nets: ElectricalNet[];
|
|
70
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Semantic PCB selection types.
|
|
75
|
+
*
|
|
76
|
+
* Self-contained structural mirror of `hq.pcb.v1` (the hq-edge-owned semantic
|
|
77
|
+
* protobuf contract for `PcbSelectionService.GetSelection`). Units follow the
|
|
78
|
+
* hq.pcb.v1 convention: positions/sizes/lengths in mm, rotations in degrees,
|
|
79
|
+
* `id` is the KiCad native object identity.
|
|
80
|
+
*/
|
|
81
|
+
interface PcbPoint {
|
|
82
|
+
x: number;
|
|
83
|
+
y: number;
|
|
84
|
+
}
|
|
85
|
+
interface PcbNetRef {
|
|
86
|
+
name: string;
|
|
87
|
+
code: number;
|
|
88
|
+
}
|
|
89
|
+
interface PcbPad {
|
|
90
|
+
id: string;
|
|
91
|
+
pin: string;
|
|
92
|
+
type: string;
|
|
93
|
+
shape: string;
|
|
94
|
+
position?: PcbPoint;
|
|
95
|
+
widthMm: number;
|
|
96
|
+
heightMm: number;
|
|
97
|
+
rotationDeg: number;
|
|
98
|
+
layer: string;
|
|
99
|
+
net?: PcbNetRef;
|
|
100
|
+
}
|
|
101
|
+
interface PcbFootprint {
|
|
102
|
+
id: string;
|
|
103
|
+
reference: string;
|
|
104
|
+
value: string;
|
|
105
|
+
footprint: string;
|
|
106
|
+
position?: PcbPoint;
|
|
107
|
+
rotationDeg: number;
|
|
108
|
+
pads: PcbPad[];
|
|
109
|
+
}
|
|
110
|
+
interface PcbTrack {
|
|
111
|
+
id: string;
|
|
112
|
+
layer: string;
|
|
113
|
+
start?: PcbPoint;
|
|
114
|
+
end?: PcbPoint;
|
|
115
|
+
widthMm: number;
|
|
116
|
+
lengthMm: number;
|
|
117
|
+
net?: PcbNetRef;
|
|
118
|
+
}
|
|
119
|
+
interface PcbArc {
|
|
120
|
+
id: string;
|
|
121
|
+
layer: string;
|
|
122
|
+
start?: PcbPoint;
|
|
123
|
+
end?: PcbPoint;
|
|
124
|
+
mid?: PcbPoint;
|
|
125
|
+
net?: PcbNetRef;
|
|
126
|
+
}
|
|
127
|
+
interface PcbVia {
|
|
128
|
+
id: string;
|
|
129
|
+
layers: string[];
|
|
130
|
+
drillMm: number;
|
|
131
|
+
viaType: string;
|
|
132
|
+
start?: PcbPoint;
|
|
133
|
+
end?: PcbPoint;
|
|
134
|
+
}
|
|
135
|
+
interface PcbSegment {
|
|
136
|
+
start?: PcbPoint;
|
|
137
|
+
end?: PcbPoint;
|
|
138
|
+
}
|
|
139
|
+
interface PcbZone {
|
|
140
|
+
id: string;
|
|
141
|
+
layer: string;
|
|
142
|
+
net?: PcbNetRef;
|
|
143
|
+
outline: PcbSegment[];
|
|
144
|
+
}
|
|
145
|
+
interface PcbShape {
|
|
146
|
+
id: string;
|
|
147
|
+
layer: string;
|
|
148
|
+
shapeType: string;
|
|
149
|
+
start?: PcbPoint;
|
|
150
|
+
end?: PcbPoint;
|
|
151
|
+
center?: PcbPoint;
|
|
152
|
+
radiusMm: number;
|
|
153
|
+
mid?: PcbPoint;
|
|
154
|
+
widthMm: number;
|
|
155
|
+
}
|
|
156
|
+
interface PcbText {
|
|
157
|
+
id: string;
|
|
158
|
+
layer: string;
|
|
159
|
+
text: string;
|
|
160
|
+
position?: PcbPoint;
|
|
161
|
+
rotationDeg: number;
|
|
162
|
+
hJustify: string;
|
|
163
|
+
vJustify: string;
|
|
164
|
+
}
|
|
165
|
+
interface PcbDimension {
|
|
166
|
+
id: string;
|
|
167
|
+
layer: string;
|
|
168
|
+
start?: PcbPoint;
|
|
169
|
+
end?: PcbPoint;
|
|
170
|
+
value: string;
|
|
171
|
+
dimType: string;
|
|
172
|
+
}
|
|
173
|
+
interface PcbGroup {
|
|
174
|
+
id: string;
|
|
175
|
+
itemIds: string[];
|
|
176
|
+
}
|
|
177
|
+
interface PcbSelection {
|
|
178
|
+
footprints: PcbFootprint[];
|
|
179
|
+
pads: PcbPad[];
|
|
180
|
+
tracks: PcbTrack[];
|
|
181
|
+
arcs: PcbArc[];
|
|
182
|
+
vias: PcbVia[];
|
|
183
|
+
zones: PcbZone[];
|
|
184
|
+
shapes: PcbShape[];
|
|
185
|
+
texts: PcbText[];
|
|
186
|
+
dimensions: PcbDimension[];
|
|
187
|
+
groups: PcbGroup[];
|
|
188
|
+
nets: PcbNetRef[];
|
|
189
|
+
}
|
|
71
190
|
/**
|
|
72
191
|
* Semantic error categories for netlist retrieval. Mirrors the gRPC status
|
|
73
192
|
* contract of `hq.ir.schematic.v1.NetListService` so an agent can distinguish
|
|
@@ -174,6 +293,12 @@ interface EdaHostClient {
|
|
|
174
293
|
getEdaHostInfo(options?: EdaHostRequestOptions): Promise<EdaHostInfo>;
|
|
175
294
|
/** Capabilities the host currently provides. */
|
|
176
295
|
getEdaHostCapabilities(options?: EdaHostRequestOptions): Promise<EdaHostCapability[]>;
|
|
296
|
+
/**
|
|
297
|
+
* Semantic PCB selection of the current PCB editor (hq.pcb.v1
|
|
298
|
+
* PcbSelectionService.GetSelection bridged through hq-edge). An empty
|
|
299
|
+
* selection resolves to an all-empty `PcbSelection` — never an error.
|
|
300
|
+
*/
|
|
301
|
+
getPcbSelection(options?: EdaHostRequestOptions): Promise<PcbSelection>;
|
|
177
302
|
}
|
|
178
303
|
/**
|
|
179
304
|
* Extract the semantic `SchematicNetlist` from an hq-edge netlist body.
|
|
@@ -248,4 +373,4 @@ declare module '@deepseek-ai/cordis' {
|
|
|
248
373
|
*/
|
|
249
374
|
declare function apply(ctx: Context, config?: Partial<EdaHostConfig>): () => void;
|
|
250
375
|
//#endregion
|
|
251
|
-
export { type EdaHostCapability, type EdaHostClient, type EdaHostConfig, type EdaHostExecutable, type EdaHostIdentity, type EdaHostInfo, type EdaHostInstallation, type EdaHostRequestOptions, type EdaHostType, type ElectricalNet, type ElectricalType, NetlistError, type NetlistErrorKind, type PinDefinition, type PinReference, type SchematicComponent, type SchematicNetlist, apply, inject, name, parseNetlistBody };
|
|
376
|
+
export { type EdaHostCapability, type EdaHostClient, type EdaHostConfig, type EdaHostExecutable, type EdaHostIdentity, type EdaHostInfo, type EdaHostInstallation, type EdaHostRequestOptions, type EdaHostType, type ElectricalNet, type ElectricalType, NetlistError, type NetlistErrorKind, type PcbArc, type PcbDimension, type PcbFootprint, type PcbGroup, type PcbNetRef, type PcbPad, type PcbPoint, type PcbSegment, type PcbSelection, type PcbShape, type PcbText, type PcbTrack, type PcbVia, type PcbZone, type PinDefinition, type PinReference, type SchematicComponent, type SchematicNetlist, apply, inject, name, parseNetlistBody };
|
package/lib/index.mjs
CHANGED
|
@@ -9,12 +9,14 @@ const SCOPE_ROUTE = {
|
|
|
9
9
|
function resolveEdaHostConfig(config, env = process.env) {
|
|
10
10
|
const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? "";
|
|
11
11
|
const pathPrefix = config?.netlistPathPrefix ?? env.HQ_EDGE_NETLIST_PATH ?? "/api/v1/netlist";
|
|
12
|
+
const pcbSelectionPrefix = config?.pcbSelectionPathPrefix ?? env.HQ_EDGE_PCB_SELECTION_PATH ?? "/api/v1/pcb-selection";
|
|
12
13
|
const hostPrefix = config?.hostPathPrefix ?? env.HQ_EDGE_HOST_PATH ?? "/api/v1/host";
|
|
13
14
|
const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS;
|
|
14
15
|
const timeout = Number.parseInt(String(timeoutRaw ?? ""), 10);
|
|
15
16
|
return {
|
|
16
17
|
hqEdgeBaseUrl: baseUrl,
|
|
17
18
|
netlistPathPrefix: pathPrefix,
|
|
19
|
+
pcbSelectionPathPrefix: pcbSelectionPrefix,
|
|
18
20
|
hostPathPrefix: hostPrefix,
|
|
19
21
|
requestTimeoutMs: Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS
|
|
20
22
|
};
|
|
@@ -27,6 +29,10 @@ function hasHost(config) {
|
|
|
27
29
|
function netlistUrlOf(config, scope) {
|
|
28
30
|
return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.netlistPathPrefix ?? "/api/v1/netlist").replace(/^\/+|\/+$/g, "")}${SCOPE_ROUTE[scope]}`;
|
|
29
31
|
}
|
|
32
|
+
/** Build the absolute URL for the PCB selection route. */
|
|
33
|
+
function pcbSelectionUrlOf(config) {
|
|
34
|
+
return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.pcbSelectionPathPrefix ?? "/api/v1/pcb-selection").replace(/^\/+|\/+$/g, "")}`;
|
|
35
|
+
}
|
|
30
36
|
const HOST_ROUTE = {
|
|
31
37
|
info: "/info",
|
|
32
38
|
capabilities: "/capabilities"
|
|
@@ -185,6 +191,23 @@ function createEdaHostClient(config, deps = {}) {
|
|
|
185
191
|
async function fetchScope(scope, options) {
|
|
186
192
|
return parseNetlistBody(await getJson(netlistUrlOf(resolveConfig(), scope), options, "netlist"));
|
|
187
193
|
}
|
|
194
|
+
function parsePcbSelection(value) {
|
|
195
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new NetlistError("INTERNAL", "eda-host: malformed PCB selection response from hq-edge");
|
|
196
|
+
const asArray = (v) => Array.isArray(v) ? v : [];
|
|
197
|
+
return {
|
|
198
|
+
footprints: asArray(value.footprints),
|
|
199
|
+
pads: asArray(value.pads),
|
|
200
|
+
tracks: asArray(value.tracks),
|
|
201
|
+
arcs: asArray(value.arcs),
|
|
202
|
+
vias: asArray(value.vias),
|
|
203
|
+
zones: asArray(value.zones),
|
|
204
|
+
shapes: asArray(value.shapes),
|
|
205
|
+
texts: asArray(value.texts),
|
|
206
|
+
dimensions: asArray(value.dimensions),
|
|
207
|
+
groups: asArray(value.groups),
|
|
208
|
+
nets: asArray(value.nets)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
188
211
|
return {
|
|
189
212
|
getSelectionNetlist: (options) => fetchScope("selection", options),
|
|
190
213
|
getProjectNetlist: (options) => fetchScope("project", options),
|
|
@@ -198,6 +221,9 @@ function createEdaHostClient(config, deps = {}) {
|
|
|
198
221
|
const body = await getJson(hostUrlOf(resolveConfig(), "capabilities"), options, "host capabilities");
|
|
199
222
|
if (!body || !Array.isArray(body.capabilities)) throw new NetlistError("INTERNAL", "eda-host: malformed host capabilities response from hq-edge");
|
|
200
223
|
return body.capabilities.filter((c) => typeof c === "string");
|
|
224
|
+
},
|
|
225
|
+
getPcbSelection: async (options) => {
|
|
226
|
+
return parsePcbSelection(await getJson(pcbSelectionUrlOf(resolveConfig()), options, "pcb selection"));
|
|
201
227
|
}
|
|
202
228
|
};
|
|
203
229
|
}
|
|
@@ -298,51 +324,77 @@ function createNetListTools(env) {
|
|
|
298
324
|
* a claim that the host can provide it, nothing more.
|
|
299
325
|
*/
|
|
300
326
|
function createEdaHostTools(env) {
|
|
301
|
-
return [
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
327
|
+
return [
|
|
328
|
+
defineTool({
|
|
329
|
+
name: "get_eda_host_info",
|
|
330
|
+
description: "Describe the EDA host currently connected through hq-edge (DSH → dsh-eda-host → hq-edge → EDA host). Returns { ok, info: { identity: { hostType, hostName, version }, installation: { applicationPath, executables[]: { name, path } } } }. Use it when you need factual information about the current EDA environment, such as \"what EDA host am I connected to\", \"what version is it\", \"where is it installed\", or \"where is kicad-cli\". The returned information is authoritative host-provided ground truth. " + ERROR_SEMANTICS,
|
|
331
|
+
parameters: {},
|
|
332
|
+
output: {
|
|
333
|
+
schema: { type: "json" },
|
|
334
|
+
render: renderJson
|
|
335
|
+
},
|
|
336
|
+
async execute(_args, exec) {
|
|
337
|
+
try {
|
|
338
|
+
const options = exec?.signal ? { signal: exec.signal } : {};
|
|
339
|
+
return asJson({
|
|
340
|
+
ok: true,
|
|
341
|
+
info: await env.client.getEdaHostInfo(options)
|
|
342
|
+
});
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return asJson({
|
|
345
|
+
ok: false,
|
|
346
|
+
error: failureOf(err)
|
|
347
|
+
});
|
|
348
|
+
}
|
|
321
349
|
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
350
|
+
}),
|
|
351
|
+
defineTool({
|
|
352
|
+
name: "get_pcb_selection",
|
|
353
|
+
description: "Read the semantic PCB selection from the current PCB editor through hq-edge (DSH → dsh-eda-host → hq-edge → EDA host). Returns { ok, selection: { footprints[], pads[], tracks[], arcs[], vias[], zones[], shapes[], texts[], dimensions[], groups[], nets[] } }. Each footprint has reference, value, footprint, position {x,y} in mm, rotationDeg and pads[] (pin, type, shape, position, widthMm, heightMm, rotationDeg, layer, net{name, code}); tracks have layer, start/end in mm, widthMm, lengthMm and net; vias have layers[], drillMm, viaType and start/end; zones have layer, net and outline segments. Every object carries id — the EDA-host native object identity. IMPORTANT: ok:true with all-empty arrays is a VALID empty selection (nothing selected) — do not treat it as a failure. " + ERROR_SEMANTICS,
|
|
354
|
+
parameters: {},
|
|
355
|
+
output: {
|
|
356
|
+
schema: { type: "json" },
|
|
357
|
+
render: renderJson
|
|
358
|
+
},
|
|
359
|
+
async execute(_args, exec) {
|
|
360
|
+
try {
|
|
361
|
+
const options = exec?.signal ? { signal: exec.signal } : {};
|
|
362
|
+
return asJson({
|
|
363
|
+
ok: true,
|
|
364
|
+
selection: await env.client.getPcbSelection(options)
|
|
365
|
+
});
|
|
366
|
+
} catch (err) {
|
|
367
|
+
return asJson({
|
|
368
|
+
ok: false,
|
|
369
|
+
error: failureOf(err)
|
|
370
|
+
});
|
|
371
|
+
}
|
|
343
372
|
}
|
|
344
|
-
}
|
|
345
|
-
|
|
373
|
+
}),
|
|
374
|
+
defineTool({
|
|
375
|
+
name: "get_eda_host_capabilities",
|
|
376
|
+
description: "List the capabilities the CURRENT EDA host provides, using EDA-independent capability identifiers such as EDA_HOST_CAPABILITY_NETLIST, EDA_HOST_CAPABILITY_PCB or EDA_HOST_CAPABILITY_BOM. Returns { ok, capabilities: string[] }. This is DISCOVERY, not execution: it reports capabilities already provided by the connected EDA host. Treat the returned capability list as the authoritative runtime contract for the current host. Do not assume a capability is available merely because the EDA application is generally known to support it. Unknown capability identifiers MUST be treated as unsupported. " + ERROR_SEMANTICS,
|
|
377
|
+
parameters: {},
|
|
378
|
+
output: {
|
|
379
|
+
schema: { type: "json" },
|
|
380
|
+
render: renderJson
|
|
381
|
+
},
|
|
382
|
+
async execute(_args, exec) {
|
|
383
|
+
try {
|
|
384
|
+
const options = exec?.signal ? { signal: exec.signal } : {};
|
|
385
|
+
return asJson({
|
|
386
|
+
ok: true,
|
|
387
|
+
capabilities: await env.client.getEdaHostCapabilities(options)
|
|
388
|
+
});
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return asJson({
|
|
391
|
+
ok: false,
|
|
392
|
+
error: failureOf(err)
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
];
|
|
346
398
|
}
|
|
347
399
|
//#endregion
|
|
348
400
|
//#region src/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-eda-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
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.
|
|
25
|
+
"@huaqiu/dsh-plugin-log": "0.4.2"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"lib",
|
package/src/client.ts
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { DEFAULT_REQUEST_TIMEOUT_MS, hostUrlOf, netlistUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
|
|
13
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, hostUrlOf, netlistUrlOf, pcbSelectionUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
|
|
14
14
|
import {
|
|
15
15
|
NetlistError,
|
|
16
16
|
type EdaHostCapability,
|
|
17
17
|
type EdaHostInfo,
|
|
18
|
+
type PcbSelection,
|
|
18
19
|
type SchematicNetlist,
|
|
19
20
|
} from './types.js'
|
|
20
21
|
|
|
@@ -51,6 +52,12 @@ export interface EdaHostClient {
|
|
|
51
52
|
getEdaHostInfo(options?: EdaHostRequestOptions): Promise<EdaHostInfo>
|
|
52
53
|
/** Capabilities the host currently provides. */
|
|
53
54
|
getEdaHostCapabilities(options?: EdaHostRequestOptions): Promise<EdaHostCapability[]>
|
|
55
|
+
/**
|
|
56
|
+
* Semantic PCB selection of the current PCB editor (hq.pcb.v1
|
|
57
|
+
* PcbSelectionService.GetSelection bridged through hq-edge). An empty
|
|
58
|
+
* selection resolves to an all-empty `PcbSelection` — never an error.
|
|
59
|
+
*/
|
|
60
|
+
getPcbSelection(options?: EdaHostRequestOptions): Promise<PcbSelection>
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
/** HTTP status → semantic error kind (see routes/edaHostStatus.ts on hq-edge). */
|
|
@@ -260,6 +267,32 @@ export function createEdaHostClient(
|
|
|
260
267
|
return parseNetlistBody(await getJson(url, options, 'netlist'))
|
|
261
268
|
}
|
|
262
269
|
|
|
270
|
+
function parsePcbSelection(value: unknown): PcbSelection {
|
|
271
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
272
|
+
throw new NetlistError(
|
|
273
|
+
'INTERNAL',
|
|
274
|
+
'eda-host: malformed PCB selection response from hq-edge',
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const asArray = (v: unknown): unknown[] => (Array.isArray(v) ? v : [])
|
|
279
|
+
|
|
280
|
+
// proto3 JSON omits empty arrays; every field is a repeated list.
|
|
281
|
+
return {
|
|
282
|
+
footprints: asArray((value as Record<string, unknown>).footprints),
|
|
283
|
+
pads: asArray((value as Record<string, unknown>).pads),
|
|
284
|
+
tracks: asArray((value as Record<string, unknown>).tracks),
|
|
285
|
+
arcs: asArray((value as Record<string, unknown>).arcs),
|
|
286
|
+
vias: asArray((value as Record<string, unknown>).vias),
|
|
287
|
+
zones: asArray((value as Record<string, unknown>).zones),
|
|
288
|
+
shapes: asArray((value as Record<string, unknown>).shapes),
|
|
289
|
+
texts: asArray((value as Record<string, unknown>).texts),
|
|
290
|
+
dimensions: asArray((value as Record<string, unknown>).dimensions),
|
|
291
|
+
groups: asArray((value as Record<string, unknown>).groups),
|
|
292
|
+
nets: asArray((value as Record<string, unknown>).nets),
|
|
293
|
+
} as PcbSelection
|
|
294
|
+
}
|
|
295
|
+
|
|
263
296
|
return {
|
|
264
297
|
getSelectionNetlist: (options) => fetchScope('selection', options),
|
|
265
298
|
getProjectNetlist: (options) => fetchScope('project', options),
|
|
@@ -293,5 +326,12 @@ export function createEdaHostClient(
|
|
|
293
326
|
(c): c is EdaHostCapability => typeof c === 'string',
|
|
294
327
|
)
|
|
295
328
|
},
|
|
329
|
+
|
|
330
|
+
getPcbSelection: async (options) => {
|
|
331
|
+
const resolved = resolveConfig()
|
|
332
|
+
const url = pcbSelectionUrlOf(resolved)
|
|
333
|
+
const body = (await getJson(url, options, 'pcb selection')) as unknown
|
|
334
|
+
return parsePcbSelection(body)
|
|
335
|
+
},
|
|
296
336
|
}
|
|
297
337
|
}
|
package/src/config.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface EdaHostConfig {
|
|
|
15
15
|
hqEdgeBaseUrl?: string
|
|
16
16
|
/** Path prefix on the host; default "/api/v1/netlist". */
|
|
17
17
|
netlistPathPrefix?: string
|
|
18
|
+
/** Path prefix for PCB selection; default "/api/v1/pcb-selection". */
|
|
19
|
+
pcbSelectionPathPrefix?: string
|
|
18
20
|
/** Path prefix for host discovery; default "/api/v1/host". */
|
|
19
21
|
hostPathPrefix?: string
|
|
20
22
|
/**
|
|
@@ -30,6 +32,8 @@ export interface EdaHostConfig {
|
|
|
30
32
|
|
|
31
33
|
export const DEFAULT_NETLIST_PATH_PREFIX = '/api/v1/netlist'
|
|
32
34
|
|
|
35
|
+
export const DEFAULT_PCB_SELECTION_PATH_PREFIX = '/api/v1/pcb-selection'
|
|
36
|
+
|
|
33
37
|
export const DEFAULT_HOST_PATH_PREFIX = '/api/v1/host'
|
|
34
38
|
|
|
35
39
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
|
|
@@ -50,6 +54,10 @@ export function resolveEdaHostConfig(
|
|
|
50
54
|
const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? ''
|
|
51
55
|
const pathPrefix =
|
|
52
56
|
config?.netlistPathPrefix ?? env.HQ_EDGE_NETLIST_PATH ?? DEFAULT_NETLIST_PATH_PREFIX
|
|
57
|
+
const pcbSelectionPrefix =
|
|
58
|
+
config?.pcbSelectionPathPrefix ??
|
|
59
|
+
env.HQ_EDGE_PCB_SELECTION_PATH ??
|
|
60
|
+
DEFAULT_PCB_SELECTION_PATH_PREFIX
|
|
53
61
|
const hostPrefix =
|
|
54
62
|
config?.hostPathPrefix ?? env.HQ_EDGE_HOST_PATH ?? DEFAULT_HOST_PATH_PREFIX
|
|
55
63
|
const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS
|
|
@@ -57,6 +65,7 @@ export function resolveEdaHostConfig(
|
|
|
57
65
|
return {
|
|
58
66
|
hqEdgeBaseUrl: baseUrl,
|
|
59
67
|
netlistPathPrefix: pathPrefix,
|
|
68
|
+
pcbSelectionPathPrefix: pcbSelectionPrefix,
|
|
60
69
|
hostPathPrefix: hostPrefix,
|
|
61
70
|
requestTimeoutMs:
|
|
62
71
|
Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS,
|
|
@@ -75,6 +84,16 @@ export function netlistUrlOf(config: EdaHostConfig, scope: NetlistScope): string
|
|
|
75
84
|
return `${base}/${prefix}${SCOPE_ROUTE[scope]}`
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
/** Build the absolute URL for the PCB selection route. */
|
|
88
|
+
export function pcbSelectionUrlOf(config: EdaHostConfig): string {
|
|
89
|
+
const base = (config.hqEdgeBaseUrl ?? '').replace(/\/+$/, '')
|
|
90
|
+
const prefix = (config.pcbSelectionPathPrefix ?? DEFAULT_PCB_SELECTION_PATH_PREFIX).replace(
|
|
91
|
+
/^\/+|\/+$/g,
|
|
92
|
+
'',
|
|
93
|
+
)
|
|
94
|
+
return `${base}/${prefix}`
|
|
95
|
+
}
|
|
96
|
+
|
|
78
97
|
/** Host discovery route suffix. */
|
|
79
98
|
export type HostRoute = 'info' | 'capabilities'
|
|
80
99
|
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* `@huaqiu/dsh-eda-host` — node plugin entry.
|
|
3
3
|
*
|
|
4
4
|
* Provides the `edaHost` service (semantic EDA-host capability) and registers
|
|
5
|
-
*
|
|
5
|
+
* six agent tools:
|
|
6
6
|
*
|
|
7
7
|
* get_project_netlist complete project netlist
|
|
8
8
|
* get_selection_netlist currently selected components netlist
|
|
9
9
|
* get_active_page_netlist active schematic page netlist
|
|
10
|
+
* get_pcb_selection semantic PCB selection of the current PCB editor
|
|
10
11
|
* get_eda_host_info which EDA host, version, installation
|
|
11
12
|
* get_eda_host_capabilities what the current host can actually do
|
|
12
13
|
*
|
|
@@ -63,6 +64,20 @@ export type {
|
|
|
63
64
|
ElectricalNet,
|
|
64
65
|
ElectricalType,
|
|
65
66
|
NetlistErrorKind,
|
|
67
|
+
PcbArc,
|
|
68
|
+
PcbDimension,
|
|
69
|
+
PcbFootprint,
|
|
70
|
+
PcbGroup,
|
|
71
|
+
PcbNetRef,
|
|
72
|
+
PcbPad,
|
|
73
|
+
PcbPoint,
|
|
74
|
+
PcbSegment,
|
|
75
|
+
PcbSelection,
|
|
76
|
+
PcbShape,
|
|
77
|
+
PcbText,
|
|
78
|
+
PcbTrack,
|
|
79
|
+
PcbVia,
|
|
80
|
+
PcbZone,
|
|
66
81
|
PinDefinition,
|
|
67
82
|
PinReference,
|
|
68
83
|
SchematicComponent,
|
package/src/tools.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
26
26
|
import type { EdaHostClient, EdaHostRequestOptions } from './client.js'
|
|
27
|
-
import { NetlistError, type EdaHostCapability, type EdaHostInfo, type SchematicNetlist } from './types.js'
|
|
27
|
+
import { NetlistError, type EdaHostCapability, type EdaHostInfo, type PcbSelection, type SchematicNetlist } from './types.js'
|
|
28
28
|
|
|
29
29
|
/** Structural alias of the DSH `JsonValue`. */
|
|
30
30
|
type Json = string | number | boolean | null | Json[] | { [key: string]: Json }
|
|
@@ -62,6 +62,10 @@ type HostCapabilitiesResult =
|
|
|
62
62
|
| { ok: true; capabilities: EdaHostCapability[] }
|
|
63
63
|
| { ok: false; error: { kind: string; message: string } }
|
|
64
64
|
|
|
65
|
+
type PcbSelectionResult =
|
|
66
|
+
| { ok: true; selection: PcbSelection }
|
|
67
|
+
| { ok: false; error: { kind: string; message: string } }
|
|
68
|
+
|
|
65
69
|
/**
|
|
66
70
|
* Every failure kind, and what the agent should do about it.
|
|
67
71
|
*
|
|
@@ -192,6 +196,36 @@ export function createNetListTools(env: NetlistToolEnv) {
|
|
|
192
196
|
},
|
|
193
197
|
}),
|
|
194
198
|
|
|
199
|
+
defineTool({
|
|
200
|
+
name: 'get_pcb_selection',
|
|
201
|
+
description:
|
|
202
|
+
`Read the semantic PCB selection from the current PCB editor through hq-edge ` +
|
|
203
|
+
`(DSH → dsh-eda-host → hq-edge → EDA host). Returns { ok, selection: { footprints[], ` +
|
|
204
|
+
`pads[], tracks[], arcs[], vias[], zones[], shapes[], texts[], dimensions[], groups[], ` +
|
|
205
|
+
`nets[] } }. ` +
|
|
206
|
+
`Each footprint has reference, value, footprint, position {x,y} in mm, rotationDeg and ` +
|
|
207
|
+
`pads[] (pin, type, shape, position, widthMm, heightMm, rotationDeg, layer, net{name, ` +
|
|
208
|
+
`code}); tracks have layer, start/end in mm, widthMm, lengthMm and net; vias have ` +
|
|
209
|
+
`layers[], drillMm, viaType and start/end; zones have layer, net and outline segments. ` +
|
|
210
|
+
`Every object carries id — the EDA-host native object identity. ` +
|
|
211
|
+
`IMPORTANT: ok:true with all-empty arrays is a VALID empty selection (nothing selected) ` +
|
|
212
|
+
`— do not treat it as a failure. ` +
|
|
213
|
+
ERROR_SEMANTICS,
|
|
214
|
+
parameters: {},
|
|
215
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
216
|
+
async execute(_args: unknown, exec: ToolExecLike): Promise<Json> {
|
|
217
|
+
try {
|
|
218
|
+
const options: EdaHostRequestOptions = exec?.signal
|
|
219
|
+
? { signal: exec.signal }
|
|
220
|
+
: {}
|
|
221
|
+
const selection = await env.client.getPcbSelection(options)
|
|
222
|
+
return asJson<PcbSelectionResult>({ ok: true, selection })
|
|
223
|
+
} catch (err) {
|
|
224
|
+
return asJson<PcbSelectionResult>({ ok: false, error: failureOf(err) })
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
|
|
195
229
|
defineTool({
|
|
196
230
|
name: 'get_eda_host_capabilities',
|
|
197
231
|
description:
|
package/src/types.ts
CHANGED
|
@@ -54,6 +54,138 @@ export interface SchematicNetlist {
|
|
|
54
54
|
nets: ElectricalNet[]
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Semantic PCB selection types.
|
|
59
|
+
*
|
|
60
|
+
* Self-contained structural mirror of `hq.pcb.v1` (the hq-edge-owned semantic
|
|
61
|
+
* protobuf contract for `PcbSelectionService.GetSelection`). Units follow the
|
|
62
|
+
* hq.pcb.v1 convention: positions/sizes/lengths in mm, rotations in degrees,
|
|
63
|
+
* `id` is the KiCad native object identity.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
export interface PcbPoint {
|
|
67
|
+
x: number
|
|
68
|
+
y: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PcbNetRef {
|
|
72
|
+
name: string
|
|
73
|
+
code: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PcbPad {
|
|
77
|
+
id: string
|
|
78
|
+
pin: string
|
|
79
|
+
type: string
|
|
80
|
+
shape: string
|
|
81
|
+
position?: PcbPoint
|
|
82
|
+
widthMm: number
|
|
83
|
+
heightMm: number
|
|
84
|
+
rotationDeg: number
|
|
85
|
+
layer: string
|
|
86
|
+
net?: PcbNetRef
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PcbFootprint {
|
|
90
|
+
id: string
|
|
91
|
+
reference: string
|
|
92
|
+
value: string
|
|
93
|
+
footprint: string
|
|
94
|
+
position?: PcbPoint
|
|
95
|
+
rotationDeg: number
|
|
96
|
+
pads: PcbPad[]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface PcbTrack {
|
|
100
|
+
id: string
|
|
101
|
+
layer: string
|
|
102
|
+
start?: PcbPoint
|
|
103
|
+
end?: PcbPoint
|
|
104
|
+
widthMm: number
|
|
105
|
+
lengthMm: number
|
|
106
|
+
net?: PcbNetRef
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface PcbArc {
|
|
110
|
+
id: string
|
|
111
|
+
layer: string
|
|
112
|
+
start?: PcbPoint
|
|
113
|
+
end?: PcbPoint
|
|
114
|
+
mid?: PcbPoint
|
|
115
|
+
net?: PcbNetRef
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface PcbVia {
|
|
119
|
+
id: string
|
|
120
|
+
layers: string[]
|
|
121
|
+
drillMm: number
|
|
122
|
+
viaType: string
|
|
123
|
+
start?: PcbPoint
|
|
124
|
+
end?: PcbPoint
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface PcbSegment {
|
|
128
|
+
start?: PcbPoint
|
|
129
|
+
end?: PcbPoint
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface PcbZone {
|
|
133
|
+
id: string
|
|
134
|
+
layer: string
|
|
135
|
+
net?: PcbNetRef
|
|
136
|
+
outline: PcbSegment[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface PcbShape {
|
|
140
|
+
id: string
|
|
141
|
+
layer: string
|
|
142
|
+
shapeType: string
|
|
143
|
+
start?: PcbPoint
|
|
144
|
+
end?: PcbPoint
|
|
145
|
+
center?: PcbPoint
|
|
146
|
+
radiusMm: number
|
|
147
|
+
mid?: PcbPoint
|
|
148
|
+
widthMm: number
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface PcbText {
|
|
152
|
+
id: string
|
|
153
|
+
layer: string
|
|
154
|
+
text: string
|
|
155
|
+
position?: PcbPoint
|
|
156
|
+
rotationDeg: number
|
|
157
|
+
hJustify: string
|
|
158
|
+
vJustify: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface PcbDimension {
|
|
162
|
+
id: string
|
|
163
|
+
layer: string
|
|
164
|
+
start?: PcbPoint
|
|
165
|
+
end?: PcbPoint
|
|
166
|
+
value: string
|
|
167
|
+
dimType: string
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface PcbGroup {
|
|
171
|
+
id: string
|
|
172
|
+
itemIds: string[]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface PcbSelection {
|
|
176
|
+
footprints: PcbFootprint[]
|
|
177
|
+
pads: PcbPad[]
|
|
178
|
+
tracks: PcbTrack[]
|
|
179
|
+
arcs: PcbArc[]
|
|
180
|
+
vias: PcbVia[]
|
|
181
|
+
zones: PcbZone[]
|
|
182
|
+
shapes: PcbShape[]
|
|
183
|
+
texts: PcbText[]
|
|
184
|
+
dimensions: PcbDimension[]
|
|
185
|
+
groups: PcbGroup[]
|
|
186
|
+
nets: PcbNetRef[]
|
|
187
|
+
}
|
|
188
|
+
|
|
57
189
|
/**
|
|
58
190
|
* Semantic error categories for netlist retrieval. Mirrors the gRPC status
|
|
59
191
|
* contract of `hq.ir.schematic.v1.NetListService` so an agent can distinguish
|