@huaqiu/dsh-eda-host 0.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/cordis.patch.yml +8 -0
- package/lib/index.d.mts +112 -0
- package/lib/index.mjs +212 -0
- package/package.json +40 -0
- package/src/client.ts +90 -0
- package/src/config.ts +54 -0
- package/src/index.ts +135 -0
- package/src/tools.ts +131 -0
- package/src/types.ts +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 深圳华秋智联股份有限公司
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @huaqiu/dsh-eda-host
|
|
2
|
+
|
|
3
|
+
DSH plugin exposing the current EDA host's schematic netlist to agents through
|
|
4
|
+
hq-edge.
|
|
5
|
+
|
|
6
|
+
## What it provides
|
|
7
|
+
|
|
8
|
+
| Tool | Scope |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `get_project_netlist` | complete logical netlist of the current project |
|
|
11
|
+
| `get_selection_netlist` | netlist of the currently selected components |
|
|
12
|
+
| `get_active_page_netlist` | netlist of the active schematic page |
|
|
13
|
+
|
|
14
|
+
Each tool returns lossless JSON:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"ok": true,
|
|
19
|
+
"scope": "project",
|
|
20
|
+
"netlist": {
|
|
21
|
+
"components": [
|
|
22
|
+
{ "referenceDesignators": ["R1"], "value": "10k", "footprint": "R_0603",
|
|
23
|
+
"manufacturerPartNumber": "", "description": "", "pins": [
|
|
24
|
+
{ "pinNumber": "1", "pinName": "1", "electricalType": "ELECTRICAL_TYPE_PASSIVE" }
|
|
25
|
+
] }
|
|
26
|
+
],
|
|
27
|
+
"nets": [
|
|
28
|
+
{ "name": "GND", "pinReferences": [
|
|
29
|
+
{ "referenceDesignator": "R1", "pinNumber": "2" }
|
|
30
|
+
] }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A valid-but-empty design is `ok: true` with empty `components`/`nets` — it is
|
|
37
|
+
NOT an error. On failure the tool returns `ok: false` with a semantic
|
|
38
|
+
`error.kind`:
|
|
39
|
+
|
|
40
|
+
- `FAILED_PRECONDITION` — no EDA host / no live editor (ask the user to open
|
|
41
|
+
the design first).
|
|
42
|
+
- `UNIMPLEMENTED` — the host does not support this scope (e.g. active page on
|
|
43
|
+
KiCad). Do not retry.
|
|
44
|
+
- `UNAVAILABLE` — hq-edge host unreachable.
|
|
45
|
+
- `INTERNAL` — host-side failure.
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
DSH
|
|
51
|
+
↓
|
|
52
|
+
dsh-eda-host (this plugin: DSH integration only, self-contained)
|
|
53
|
+
↓ hq-edge HTTP
|
|
54
|
+
hq-edge (semantic protocol + orchestration; /api/v1/netlist/*)
|
|
55
|
+
↓ gRPC
|
|
56
|
+
EDA Host (hq.ir.schematic.v1.NetListService — KiCad / HQ EDA)
|
|
57
|
+
↓
|
|
58
|
+
native EDA model
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- No `@hqedge/*` dependency: the base URL arrives as overlay config
|
|
62
|
+
(`hqEdgeBaseUrl`, delivered by the hq-edge supervisor) or `HQ_EDGE_BASE_URL`
|
|
63
|
+
env fallback.
|
|
64
|
+
- No KiCad code, no schematic parsing, no host IPC in this package.
|
|
65
|
+
- Errors are propagated with semantic kinds — never converted into fake empty
|
|
66
|
+
netlists.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# DSH bundle patch: inserts the Huaqiu EDA host netlist tool plugin.
|
|
2
|
+
# The entry inject must match the node-half module contract (`src/index.ts` —
|
|
3
|
+
# export const inject). The tool's apply() reads the `tools` service; the
|
|
4
|
+
# hq-edge supervisor delivers the base URL as overlay config.
|
|
5
|
+
- insert:
|
|
6
|
+
- id: huaqiu-dsh-eda-host
|
|
7
|
+
name: '@huaqiu/dsh-eda-host'
|
|
8
|
+
inject: ['tools']
|
package/lib/index.d.mts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
//#region src/config.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Runtime config resolution for `@huaqiu/dsh-eda-host`.
|
|
5
|
+
*
|
|
6
|
+
* The plugin never opens a direct connection to the EDA host — it only talks
|
|
7
|
+
* to `hq-edge`, which owns the host session. The base URL is delivered by the
|
|
8
|
+
* hq-edge supervisor as overlay config (`hqEdgeBaseUrl`), with the
|
|
9
|
+
* `HQ_EDGE_BASE_URL` env as fallback for non-supervisor installs. This mirrors
|
|
10
|
+
* the `@huaqiu/dsh-auth` host-config convention.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
interface EdaHostConfig {
|
|
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/netlist". */
|
|
18
|
+
netlistPathPrefix?: string;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/types.d.ts
|
|
22
|
+
/**
|
|
23
|
+
* Semantic netlist types for `@huaqiu/dsh-eda-host`.
|
|
24
|
+
*
|
|
25
|
+
* Self-contained structural mirror of `hq.ir.schematic.v1` (the hq-edge-owned
|
|
26
|
+
* semantic protobuf contract). The plugin intentionally does NOT depend on
|
|
27
|
+
* `@hqedge/*` — this file is the only place the wire shapes are named, so
|
|
28
|
+
* keeping them aligned with the proto is a one-file concern.
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
/** Electrical type of a pin — mirrors `hq.ir.logical.v1.ElectricalType`. */
|
|
33
|
+
type ElectricalType = 'ELECTRICAL_TYPE_UNSPECIFIED' | 'ELECTRICAL_TYPE_INPUT' | 'ELECTRICAL_TYPE_OUTPUT' | 'ELECTRICAL_TYPE_BIDIRECTIONAL' | 'ELECTRICAL_TYPE_TRISTATE' | 'ELECTRICAL_TYPE_PASSIVE' | 'ELECTRICAL_TYPE_POWER_IN' | 'ELECTRICAL_TYPE_POWER_OUT' | 'ELECTRICAL_TYPE_OPEN_COLLECTOR' | 'ELECTRICAL_TYPE_OPEN_EMITTER' | 'ELECTRICAL_TYPE_NO_CONNECT';
|
|
34
|
+
interface PinDefinition {
|
|
35
|
+
pinNumber: string;
|
|
36
|
+
pinName: string;
|
|
37
|
+
electricalType: ElectricalType;
|
|
38
|
+
}
|
|
39
|
+
interface SchematicComponent {
|
|
40
|
+
referenceDesignators: string[];
|
|
41
|
+
value: string;
|
|
42
|
+
manufacturerPartNumber: string;
|
|
43
|
+
footprint: string;
|
|
44
|
+
description: string;
|
|
45
|
+
pins: PinDefinition[];
|
|
46
|
+
}
|
|
47
|
+
interface PinReference {
|
|
48
|
+
referenceDesignator: string;
|
|
49
|
+
pinNumber: string;
|
|
50
|
+
}
|
|
51
|
+
interface ElectricalNet {
|
|
52
|
+
name: string;
|
|
53
|
+
portName?: string;
|
|
54
|
+
pinReferences: PinReference[];
|
|
55
|
+
}
|
|
56
|
+
interface SchematicNetlist {
|
|
57
|
+
components: SchematicComponent[];
|
|
58
|
+
nets: ElectricalNet[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Semantic error categories for netlist retrieval. Mirrors the gRPC status
|
|
62
|
+
* contract of `hq.ir.schematic.v1.NetListService` so an agent can distinguish
|
|
63
|
+
* "valid but empty" from "cannot answer at all".
|
|
64
|
+
*/
|
|
65
|
+
type NetlistErrorKind =
|
|
66
|
+
/** No EDA host is reachable (hq-edge not configured / host down). */
|
|
67
|
+
'FAILED_PRECONDITION' |
|
|
68
|
+
/** The host does not implement this scope (e.g. active page on KiCad). */
|
|
69
|
+
'UNIMPLEMENTED' |
|
|
70
|
+
/** Host-side runtime failure. */
|
|
71
|
+
'INTERNAL' |
|
|
72
|
+
/** Host unavailable (connection refused). */
|
|
73
|
+
'UNAVAILABLE';
|
|
74
|
+
declare class NetlistError extends Error {
|
|
75
|
+
readonly kind: NetlistErrorKind;
|
|
76
|
+
constructor(kind: NetlistErrorKind, message: string);
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/client.d.ts
|
|
80
|
+
interface EdaHostClient {
|
|
81
|
+
/** Netlist of the currently selected components. */
|
|
82
|
+
getSelectionNetlist(): Promise<SchematicNetlist>;
|
|
83
|
+
/** Complete logical netlist for the current project. */
|
|
84
|
+
getProjectNetlist(): Promise<SchematicNetlist>;
|
|
85
|
+
/** Netlist of the current active schematic page. */
|
|
86
|
+
getActivePageNetlist(): Promise<SchematicNetlist>;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/index.d.ts
|
|
90
|
+
/** Plugin id — matches package.json. */
|
|
91
|
+
declare const name = "@huaqiu/dsh-eda-host";
|
|
92
|
+
/** Cordis services this half depends on. */
|
|
93
|
+
declare const inject: readonly ["tools"];
|
|
94
|
+
declare module '@deepseek-ai/cordis' {
|
|
95
|
+
interface Context {
|
|
96
|
+
edaHost: EdaHostClient;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Host plugin body — provide `edaHost` and register the three netlist tools.
|
|
101
|
+
*
|
|
102
|
+
* When no host base URL is configured, tools return ok:false with
|
|
103
|
+
* error.kind "FAILED_PRECONDITION" instead of throwing at load time: the
|
|
104
|
+
* plugin can be installed in standalone DSH where hq-edge is absent, and the
|
|
105
|
+
* tools degrade to a clear semantic message.
|
|
106
|
+
*
|
|
107
|
+
* @param ctx - real cordis context (node side).
|
|
108
|
+
* @returns disposer — unregisters the tools on plugin dispose.
|
|
109
|
+
*/
|
|
110
|
+
declare function apply(ctx: Context, config?: Partial<EdaHostConfig>): () => void;
|
|
111
|
+
//#endregion
|
|
112
|
+
export { type EdaHostClient, type EdaHostConfig, type ElectricalNet, type ElectricalType, NetlistError, type NetlistErrorKind, type PinDefinition, type PinReference, type SchematicComponent, type SchematicNetlist, apply, inject, name };
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { getLogger } from "@huaqiu/dsh-plugin-log";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
const SCOPE_ROUTE = {
|
|
4
|
+
project: "/project",
|
|
5
|
+
selection: "/selection",
|
|
6
|
+
"active-page": "/active-page"
|
|
7
|
+
};
|
|
8
|
+
function resolveEdaHostConfig(config, env = process.env) {
|
|
9
|
+
return {
|
|
10
|
+
hqEdgeBaseUrl: config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? "",
|
|
11
|
+
netlistPathPrefix: config?.netlistPathPrefix ?? env.HQ_EDGE_NETLIST_PATH ?? "/api/v1/netlist"
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** True when a host base URL is available (host mode). */
|
|
15
|
+
function hasHost(config) {
|
|
16
|
+
return typeof config.hqEdgeBaseUrl === "string" && config.hqEdgeBaseUrl.trim().length > 0;
|
|
17
|
+
}
|
|
18
|
+
/** Build the absolute URL for one netlist scope. */
|
|
19
|
+
function netlistUrlOf(config, scope) {
|
|
20
|
+
return `${(config.hqEdgeBaseUrl ?? "").replace(/\/+$/, "")}/${(config.netlistPathPrefix ?? "/api/v1/netlist").replace(/^\/+|\/+$/g, "")}${SCOPE_ROUTE[scope]}`;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/types.ts
|
|
24
|
+
var NetlistError = class extends Error {
|
|
25
|
+
kind;
|
|
26
|
+
constructor(kind, message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "NetlistError";
|
|
29
|
+
this.kind = kind;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/client.ts
|
|
34
|
+
/**
|
|
35
|
+
* Netlist transport for `@huaqiu/dsh-eda-host`.
|
|
36
|
+
*
|
|
37
|
+
* DSH → dsh-eda-host → hq-edge → EDA Host is the ONLY production path. This
|
|
38
|
+
* module fetches the semantic netlist from the hq-edge netlist router and
|
|
39
|
+
* maps the HTTP status back to the semantic gRPC error categories. It never
|
|
40
|
+
* parses schematic files and never touches KiCad.
|
|
41
|
+
*
|
|
42
|
+
* @module
|
|
43
|
+
*/
|
|
44
|
+
/** HTTP status → semantic error kind (see routes/netlist.ts on hq-edge). */
|
|
45
|
+
function statusToKind(status) {
|
|
46
|
+
if (status === 412) return "FAILED_PRECONDITION";
|
|
47
|
+
if (status === 501) return "UNIMPLEMENTED";
|
|
48
|
+
if (status === 503) return "UNAVAILABLE";
|
|
49
|
+
return "INTERNAL";
|
|
50
|
+
}
|
|
51
|
+
function createEdaHostClient(config, deps = {}) {
|
|
52
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
53
|
+
async function fetchScope(scope) {
|
|
54
|
+
const url = netlistUrlOf(config, scope);
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await fetchImpl(url, {
|
|
58
|
+
method: "GET",
|
|
59
|
+
headers: { Accept: "application/json" }
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
throw new NetlistError("UNAVAILABLE", `eda-host: cannot reach hq-edge at ${url}: ${String(err?.message ?? err)}`);
|
|
63
|
+
}
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
let detail = "";
|
|
66
|
+
try {
|
|
67
|
+
const body = await response.json();
|
|
68
|
+
if (typeof body.detail === "string") detail = body.detail;
|
|
69
|
+
} catch {}
|
|
70
|
+
throw new NetlistError(statusToKind(response.status), `eda-host: netlist request failed (${response.status}${detail ? `: ${detail}` : ""})`);
|
|
71
|
+
}
|
|
72
|
+
const body = await response.json();
|
|
73
|
+
if (!body || typeof body.netlist !== "object" || body.netlist === null) throw new NetlistError("INTERNAL", "eda-host: malformed netlist response from hq-edge");
|
|
74
|
+
const netlist = body.netlist;
|
|
75
|
+
netlist.components ??= [];
|
|
76
|
+
netlist.nets ??= [];
|
|
77
|
+
return netlist;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
getSelectionNetlist: () => fetchScope("selection"),
|
|
81
|
+
getProjectNetlist: () => fetchScope("project"),
|
|
82
|
+
getActivePageNetlist: () => fetchScope("active-page")
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/tools.ts
|
|
87
|
+
/**
|
|
88
|
+
* Agent tools for `@huaqiu/dsh-eda-host`.
|
|
89
|
+
*
|
|
90
|
+
* Three semantic operations, one per netlist scope:
|
|
91
|
+
*
|
|
92
|
+
* get_project_netlist complete logical netlist of the current project
|
|
93
|
+
* get_selection_netlist netlist of the currently selected components
|
|
94
|
+
* get_active_page_netlist netlist of the active schematic page
|
|
95
|
+
*
|
|
96
|
+
* The tools are pure pass-throughs: they call the hq-edge netlist router and
|
|
97
|
+
* return the semantic `SchematicNetlist` as lossless JSON. Errors are
|
|
98
|
+
* propagated with a semantic `kind` (FAILED_PRECONDITION / UNIMPLEMENTED /
|
|
99
|
+
* INTERNAL / UNAVAILABLE) — never converted into a fake empty netlist. A
|
|
100
|
+
* valid-but-empty netlist is `ok: true` with empty `components`/`nets`.
|
|
101
|
+
*
|
|
102
|
+
* @module
|
|
103
|
+
*/
|
|
104
|
+
/** The normalized domain values are lossless-JSON plain objects. */
|
|
105
|
+
function asJson(value) {
|
|
106
|
+
return JSON.parse(JSON.stringify(value));
|
|
107
|
+
}
|
|
108
|
+
function renderJson(_args, value) {
|
|
109
|
+
return [{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: JSON.stringify(value)
|
|
112
|
+
}];
|
|
113
|
+
}
|
|
114
|
+
async function runScope(env, scope) {
|
|
115
|
+
try {
|
|
116
|
+
let netlist;
|
|
117
|
+
if (scope === "project") netlist = await env.client.getProjectNetlist();
|
|
118
|
+
else if (scope === "selection") netlist = await env.client.getSelectionNetlist();
|
|
119
|
+
else netlist = await env.client.getActivePageNetlist();
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
scope,
|
|
123
|
+
netlist
|
|
124
|
+
};
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
scope,
|
|
129
|
+
error: {
|
|
130
|
+
kind: err instanceof NetlistError ? err.kind : "INTERNAL",
|
|
131
|
+
message: String(err?.message ?? err)
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function scopeDescription(scope, extra) {
|
|
137
|
+
return "Read the current schematic netlist from the EDA host through hq-edge (DSH → dsh-eda-host → hq-edge → EDA host). " + extra + " The result is a semantic netlist JSON: { ok, scope, netlist: { components[], nets[] } }. Each component has referenceDesignators[], value, manufacturerPartNumber, footprint, description and pins[] (pinNumber, pinName, electricalType); each net has name and pinReferences[] (referenceDesignator, pinNumber). IMPORTANT SEMANTICS: ok:true with empty components/nets is a VALID empty design — do not treat it as a failure. On ok:false, error.kind distinguishes the cause: \"FAILED_PRECONDITION\" (no EDA host / no live editor — ask the user to open the design in the EDA editor first, then retry), \"UNIMPLEMENTED\" (this scope is not supported by the current host — do NOT retry; report it to the user), \"UNAVAILABLE\" (hq-edge host unreachable), \"INTERNAL\" (host-side failure). Do NOT fabricate netlist data.";
|
|
138
|
+
}
|
|
139
|
+
function createNetListTools(env) {
|
|
140
|
+
const mkTool = (scope, name, desc) => defineTool({
|
|
141
|
+
name,
|
|
142
|
+
description: desc,
|
|
143
|
+
parameters: {},
|
|
144
|
+
output: {
|
|
145
|
+
schema: { type: "json" },
|
|
146
|
+
render: renderJson
|
|
147
|
+
},
|
|
148
|
+
async execute(_args, _exec) {
|
|
149
|
+
return asJson(await runScope(env, scope));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return [
|
|
153
|
+
mkTool("project", "get_project_netlist", scopeDescription("project", "Returns the complete logical netlist for the current project (all sheets, all nets).")),
|
|
154
|
+
mkTool("selection", "get_selection_netlist", scopeDescription("selection", "Returns the netlist associated with the currently selected schematic components, including the nets they participate in (each net lists every connected pin, not only the selected ones).")),
|
|
155
|
+
mkTool("active_page", "get_active_page_netlist", scopeDescription("active_page", "Returns the netlist for the currently active schematic page. NOTE: KiCad host does not implement this scope — expect ok:false with error.kind \"UNIMPLEMENTED\"."))
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/index.ts
|
|
160
|
+
/** Plugin id — matches package.json. */
|
|
161
|
+
const name = "@huaqiu/dsh-eda-host";
|
|
162
|
+
/** Cordis services this half depends on. */
|
|
163
|
+
const inject = ["tools"];
|
|
164
|
+
const log = getLogger("dsh-eda-host");
|
|
165
|
+
/**
|
|
166
|
+
* Host plugin body — provide `edaHost` and register the three netlist tools.
|
|
167
|
+
*
|
|
168
|
+
* When no host base URL is configured, tools return ok:false with
|
|
169
|
+
* error.kind "FAILED_PRECONDITION" instead of throwing at load time: the
|
|
170
|
+
* plugin can be installed in standalone DSH where hq-edge is absent, and the
|
|
171
|
+
* tools degrade to a clear semantic message.
|
|
172
|
+
*
|
|
173
|
+
* @param ctx - real cordis context (node side).
|
|
174
|
+
* @returns disposer — unregisters the tools on plugin dispose.
|
|
175
|
+
*/
|
|
176
|
+
function apply(ctx, config = {}) {
|
|
177
|
+
if (!ctx.tools || typeof ctx.tools.register !== "function") throw new Error("@huaqiu/dsh-eda-host requires the DSH `tools` service (ctx.tools.register).");
|
|
178
|
+
const resolved = resolveEdaHostConfig(config);
|
|
179
|
+
log.info("applying dsh-eda-host node half", {
|
|
180
|
+
hasHost: hasHost(resolved),
|
|
181
|
+
hqEdgeBaseUrl: resolved.hqEdgeBaseUrl ?? null,
|
|
182
|
+
netlistPathPrefix: resolved.netlistPathPrefix
|
|
183
|
+
});
|
|
184
|
+
let client;
|
|
185
|
+
if (hasHost(resolved)) client = createEdaHostClient(resolved);
|
|
186
|
+
else client = {
|
|
187
|
+
getProjectNetlist: async () => {
|
|
188
|
+
throw new NetlistError("FAILED_PRECONDITION", "eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — netlist tools require the hq-edge EDA host bridge.");
|
|
189
|
+
},
|
|
190
|
+
getSelectionNetlist: async () => {
|
|
191
|
+
throw new NetlistError("FAILED_PRECONDITION", "eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — netlist tools require the hq-edge EDA host bridge.");
|
|
192
|
+
},
|
|
193
|
+
getActivePageNetlist: async () => {
|
|
194
|
+
throw new NetlistError("FAILED_PRECONDITION", "eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — netlist tools require the hq-edge EDA host bridge.");
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
ctx.effect(() => ctx.provide("edaHost", client));
|
|
198
|
+
const tools = createNetListTools({ client });
|
|
199
|
+
const disposers = [];
|
|
200
|
+
for (const tool of tools) disposers.push(ctx.tools.register(tool));
|
|
201
|
+
log.info("dsh-eda-host node half ready", {
|
|
202
|
+
tools: 3,
|
|
203
|
+
hostMode: hasHost(resolved)
|
|
204
|
+
});
|
|
205
|
+
return () => {
|
|
206
|
+
for (const dispose of disposers) try {
|
|
207
|
+
dispose();
|
|
208
|
+
} catch {}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
export { NetlistError, apply, inject, name };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huaqiu/dsh-eda-host",
|
|
3
|
+
"version": "0.3.20",
|
|
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.3.20"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"lib",
|
|
29
|
+
"src",
|
|
30
|
+
"cordis.patch.yml"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Netlist transport for `@huaqiu/dsh-eda-host`.
|
|
3
|
+
*
|
|
4
|
+
* DSH → dsh-eda-host → hq-edge → EDA Host is the ONLY production path. This
|
|
5
|
+
* module fetches the semantic netlist from the hq-edge netlist router and
|
|
6
|
+
* maps the HTTP status back to the semantic gRPC error categories. It never
|
|
7
|
+
* parses schematic files and never touches KiCad.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { netlistUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
|
|
13
|
+
import {
|
|
14
|
+
NetlistError,
|
|
15
|
+
type SchematicNetlist,
|
|
16
|
+
} from './types.js'
|
|
17
|
+
|
|
18
|
+
export interface EdaHostClientDeps {
|
|
19
|
+
fetchImpl?: typeof fetch
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EdaHostClient {
|
|
23
|
+
/** Netlist of the currently selected components. */
|
|
24
|
+
getSelectionNetlist(): Promise<SchematicNetlist>
|
|
25
|
+
/** Complete logical netlist for the current project. */
|
|
26
|
+
getProjectNetlist(): Promise<SchematicNetlist>
|
|
27
|
+
/** Netlist of the current active schematic page. */
|
|
28
|
+
getActivePageNetlist(): Promise<SchematicNetlist>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** HTTP status → semantic error kind (see routes/netlist.ts on hq-edge). */
|
|
32
|
+
function statusToKind(status: number): NetlistError['kind'] {
|
|
33
|
+
if (status === 412) return 'FAILED_PRECONDITION'
|
|
34
|
+
if (status === 501) return 'UNIMPLEMENTED'
|
|
35
|
+
if (status === 503) return 'UNAVAILABLE'
|
|
36
|
+
return 'INTERNAL'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createEdaHostClient(
|
|
40
|
+
config: EdaHostConfig,
|
|
41
|
+
deps: EdaHostClientDeps = {},
|
|
42
|
+
): EdaHostClient {
|
|
43
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch
|
|
44
|
+
|
|
45
|
+
async function fetchScope(scope: NetlistScope): Promise<SchematicNetlist> {
|
|
46
|
+
const url = netlistUrlOf(config, scope)
|
|
47
|
+
|
|
48
|
+
let response: Response
|
|
49
|
+
try {
|
|
50
|
+
response = await fetchImpl(url, { method: 'GET', headers: { Accept: 'application/json' } })
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// Connection-level failure: host not running / unreachable.
|
|
53
|
+
throw new NetlistError(
|
|
54
|
+
'UNAVAILABLE',
|
|
55
|
+
`eda-host: cannot reach hq-edge at ${url}: ${String((err as Error)?.message ?? err)}`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
let detail = ''
|
|
61
|
+
try {
|
|
62
|
+
const body = (await response.json()) as { detail?: unknown }
|
|
63
|
+
if (typeof body.detail === 'string') detail = body.detail
|
|
64
|
+
} catch {
|
|
65
|
+
// non-JSON error body — fall through with empty detail
|
|
66
|
+
}
|
|
67
|
+
throw new NetlistError(
|
|
68
|
+
statusToKind(response.status),
|
|
69
|
+
`eda-host: netlist request failed (${response.status}${detail ? `: ${detail}` : ''})`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Valid (possibly empty) result: { netlist: { components, nets } }.
|
|
74
|
+
const body = (await response.json()) as { netlist?: SchematicNetlist }
|
|
75
|
+
if (!body || typeof body.netlist !== 'object' || body.netlist === null) {
|
|
76
|
+
throw new NetlistError('INTERNAL', 'eda-host: malformed netlist response from hq-edge')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const netlist = body.netlist
|
|
80
|
+
netlist.components ??= []
|
|
81
|
+
netlist.nets ??= []
|
|
82
|
+
return netlist
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
getSelectionNetlist: () => fetchScope('selection'),
|
|
87
|
+
getProjectNetlist: () => fetchScope('project'),
|
|
88
|
+
getActivePageNetlist: () => fetchScope('active-page'),
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime config resolution for `@huaqiu/dsh-eda-host`.
|
|
3
|
+
*
|
|
4
|
+
* The plugin never opens a direct connection to the EDA host — it only talks
|
|
5
|
+
* to `hq-edge`, which owns the host session. The base URL is delivered by the
|
|
6
|
+
* hq-edge supervisor as overlay config (`hqEdgeBaseUrl`), with the
|
|
7
|
+
* `HQ_EDGE_BASE_URL` env as fallback for non-supervisor installs. This mirrors
|
|
8
|
+
* the `@huaqiu/dsh-auth` host-config convention.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface EdaHostConfig {
|
|
14
|
+
/** HQ Edge base URL, e.g. "http://localhost:18080". Absent → no host. */
|
|
15
|
+
hqEdgeBaseUrl?: string
|
|
16
|
+
/** Path prefix on the host; default "/api/v1/netlist". */
|
|
17
|
+
netlistPathPrefix?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_NETLIST_PATH_PREFIX = '/api/v1/netlist'
|
|
21
|
+
|
|
22
|
+
/** Scope → route suffix on the host netlist router. */
|
|
23
|
+
export type NetlistScope = 'project' | 'selection' | 'active-page'
|
|
24
|
+
|
|
25
|
+
export const SCOPE_ROUTE: Record<NetlistScope, string> = {
|
|
26
|
+
project: '/project',
|
|
27
|
+
selection: '/selection',
|
|
28
|
+
'active-page': '/active-page',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveEdaHostConfig(
|
|
32
|
+
config?: Partial<EdaHostConfig> | null,
|
|
33
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
34
|
+
): EdaHostConfig {
|
|
35
|
+
const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? ''
|
|
36
|
+
const pathPrefix =
|
|
37
|
+
config?.netlistPathPrefix ?? env.HQ_EDGE_NETLIST_PATH ?? DEFAULT_NETLIST_PATH_PREFIX
|
|
38
|
+
return {
|
|
39
|
+
hqEdgeBaseUrl: baseUrl,
|
|
40
|
+
netlistPathPrefix: pathPrefix,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True when a host base URL is available (host mode). */
|
|
45
|
+
export function hasHost(config: EdaHostConfig): boolean {
|
|
46
|
+
return typeof config.hqEdgeBaseUrl === 'string' && config.hqEdgeBaseUrl.trim().length > 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Build the absolute URL for one netlist scope. */
|
|
50
|
+
export function netlistUrlOf(config: EdaHostConfig, scope: NetlistScope): string {
|
|
51
|
+
const base = (config.hqEdgeBaseUrl ?? '').replace(/\/+$/, '')
|
|
52
|
+
const prefix = (config.netlistPathPrefix ?? DEFAULT_NETLIST_PATH_PREFIX).replace(/^\/+|\/+$/g, '')
|
|
53
|
+
return `${base}/${prefix}${SCOPE_ROUTE[scope]}`
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/dsh-eda-host` — node plugin entry.
|
|
3
|
+
*
|
|
4
|
+
* Provides the `edaHost` service (semantic EDA-host capability) and registers
|
|
5
|
+
* three agent tools:
|
|
6
|
+
*
|
|
7
|
+
* get_project_netlist complete project netlist
|
|
8
|
+
* get_selection_netlist currently selected components netlist
|
|
9
|
+
* get_active_page_netlist active schematic page netlist
|
|
10
|
+
*
|
|
11
|
+
* ── Architectural boundary (task: add-dsh-eda-host) ─────────────────────────
|
|
12
|
+
* The ONLY production request path is DSH → dsh-eda-host → hq-edge → EDA host.
|
|
13
|
+
* This plugin owns DSH integration only: it translates tool calls into hq-edge
|
|
14
|
+
* requests and returns the semantic `SchematicNetlist`. It contains no
|
|
15
|
+
* KiCad-specific logic, no schematic parsing, and no host IPC. The plugin is
|
|
16
|
+
* self-contained — no `@hqedge/*` dependency; the base URL is delivered by the
|
|
17
|
+
* hq-edge supervisor as overlay config (`hqEdgeBaseUrl`), with
|
|
18
|
+
* `HQ_EDGE_BASE_URL` as env fallback (same convention as `@huaqiu/dsh-auth`).
|
|
19
|
+
*
|
|
20
|
+
* @module @huaqiu/dsh-eda-host
|
|
21
|
+
*/
|
|
22
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
23
|
+
import { getLogger } from '@huaqiu/dsh-plugin-log'
|
|
24
|
+
import { createEdaHostClient, type EdaHostClient } from './client.js'
|
|
25
|
+
import { hasHost, resolveEdaHostConfig, type EdaHostConfig } from './config.js'
|
|
26
|
+
import { createNetListTools } from './tools.js'
|
|
27
|
+
import { NetlistError } from './types.js'
|
|
28
|
+
|
|
29
|
+
/** Plugin id — matches package.json. */
|
|
30
|
+
export const name = '@huaqiu/dsh-eda-host'
|
|
31
|
+
|
|
32
|
+
/** Cordis services this half depends on. */
|
|
33
|
+
export const inject = ['tools'] as const
|
|
34
|
+
|
|
35
|
+
export type { EdaHostConfig } from './config.js'
|
|
36
|
+
export type { EdaHostClient } from './client.js'
|
|
37
|
+
export type {
|
|
38
|
+
ElectricalNet,
|
|
39
|
+
ElectricalType,
|
|
40
|
+
NetlistErrorKind,
|
|
41
|
+
PinDefinition,
|
|
42
|
+
PinReference,
|
|
43
|
+
SchematicComponent,
|
|
44
|
+
SchematicNetlist,
|
|
45
|
+
} from './types.js'
|
|
46
|
+
export { NetlistError } from './types.js'
|
|
47
|
+
|
|
48
|
+
declare module '@deepseek-ai/cordis' {
|
|
49
|
+
interface Context {
|
|
50
|
+
edaHost: EdaHostClient
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Shared component name for the unified DSH-plugin log. */
|
|
55
|
+
const COMPONENT = 'dsh-eda-host'
|
|
56
|
+
const log = getLogger(COMPONENT)
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Host plugin body — provide `edaHost` and register the three netlist tools.
|
|
60
|
+
*
|
|
61
|
+
* When no host base URL is configured, tools return ok:false with
|
|
62
|
+
* error.kind "FAILED_PRECONDITION" instead of throwing at load time: the
|
|
63
|
+
* plugin can be installed in standalone DSH where hq-edge is absent, and the
|
|
64
|
+
* tools degrade to a clear semantic message.
|
|
65
|
+
*
|
|
66
|
+
* @param ctx - real cordis context (node side).
|
|
67
|
+
* @returns disposer — unregisters the tools on plugin dispose.
|
|
68
|
+
*/
|
|
69
|
+
export function apply(ctx: Context, config: Partial<EdaHostConfig> = {}): () => void {
|
|
70
|
+
if (!ctx.tools || typeof ctx.tools.register !== 'function') {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'@huaqiu/dsh-eda-host requires the DSH `tools` service (ctx.tools.register).',
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const resolved = resolveEdaHostConfig(config)
|
|
77
|
+
log.info('applying dsh-eda-host node half', {
|
|
78
|
+
hasHost: hasHost(resolved),
|
|
79
|
+
hqEdgeBaseUrl: resolved.hqEdgeBaseUrl ?? null,
|
|
80
|
+
netlistPathPrefix: resolved.netlistPathPrefix,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
let client: EdaHostClient
|
|
84
|
+
|
|
85
|
+
if (hasHost(resolved)) {
|
|
86
|
+
client = createEdaHostClient(resolved)
|
|
87
|
+
} else {
|
|
88
|
+
// Standalone install (no hq-edge supervisor): every scope degrades to
|
|
89
|
+
// FAILED_PRECONDITION with a clear message.
|
|
90
|
+
const unavailable: EdaHostClient = {
|
|
91
|
+
getProjectNetlist: async () => {
|
|
92
|
+
throw new NetlistError(
|
|
93
|
+
'FAILED_PRECONDITION',
|
|
94
|
+
'eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — ' +
|
|
95
|
+
'netlist tools require the hq-edge EDA host bridge.',
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
getSelectionNetlist: async () => {
|
|
99
|
+
throw new NetlistError(
|
|
100
|
+
'FAILED_PRECONDITION',
|
|
101
|
+
'eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — ' +
|
|
102
|
+
'netlist tools require the hq-edge EDA host bridge.',
|
|
103
|
+
)
|
|
104
|
+
},
|
|
105
|
+
getActivePageNetlist: async () => {
|
|
106
|
+
throw new NetlistError(
|
|
107
|
+
'FAILED_PRECONDITION',
|
|
108
|
+
'eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — ' +
|
|
109
|
+
'netlist tools require the hq-edge EDA host bridge.',
|
|
110
|
+
)
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
client = unavailable
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
ctx.effect(() => ctx.provide('edaHost', client))
|
|
117
|
+
|
|
118
|
+
const tools = createNetListTools({ client })
|
|
119
|
+
const disposers: Array<() => void> = []
|
|
120
|
+
for (const tool of tools) {
|
|
121
|
+
disposers.push(ctx.tools.register(tool))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
log.info('dsh-eda-host node half ready', { tools: 3, hostMode: hasHost(resolved) })
|
|
125
|
+
|
|
126
|
+
return () => {
|
|
127
|
+
for (const dispose of disposers) {
|
|
128
|
+
try {
|
|
129
|
+
dispose()
|
|
130
|
+
} catch {
|
|
131
|
+
// best-effort teardown
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tools for `@huaqiu/dsh-eda-host`.
|
|
3
|
+
*
|
|
4
|
+
* Three semantic operations, one per netlist scope:
|
|
5
|
+
*
|
|
6
|
+
* get_project_netlist complete logical netlist of the current project
|
|
7
|
+
* get_selection_netlist netlist of the currently selected components
|
|
8
|
+
* get_active_page_netlist netlist of the active schematic page
|
|
9
|
+
*
|
|
10
|
+
* The tools are pure pass-throughs: they call the hq-edge netlist router and
|
|
11
|
+
* return the semantic `SchematicNetlist` as lossless JSON. Errors are
|
|
12
|
+
* propagated with a semantic `kind` (FAILED_PRECONDITION / UNIMPLEMENTED /
|
|
13
|
+
* INTERNAL / UNAVAILABLE) — never converted into a fake empty netlist. A
|
|
14
|
+
* valid-but-empty netlist is `ok: true` with empty `components`/`nets`.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
20
|
+
import type { EdaHostClient } from './client.js'
|
|
21
|
+
import { NetlistError, type SchematicNetlist } from './types.js'
|
|
22
|
+
|
|
23
|
+
/** Structural alias of the DSH `JsonValue`. */
|
|
24
|
+
type Json = string | number | boolean | null | Json[] | { [key: string]: Json }
|
|
25
|
+
|
|
26
|
+
/** The normalized domain values are lossless-JSON plain objects. */
|
|
27
|
+
function asJson<T>(value: T): Json {
|
|
28
|
+
return JSON.parse(JSON.stringify(value)) as Json
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderJson(_args: unknown, value: unknown) {
|
|
32
|
+
return [{ type: 'text' as const, text: JSON.stringify(value) }]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type NetlistScopeKind = 'project' | 'selection' | 'active_page'
|
|
36
|
+
|
|
37
|
+
export interface NetlistToolEnv {
|
|
38
|
+
client: EdaHostClient
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Tool execution context (structural view of DSH's ToolRunContext). */
|
|
42
|
+
export interface ToolExecLike {
|
|
43
|
+
signal?: AbortSignal
|
|
44
|
+
callId?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ScopeResult =
|
|
48
|
+
| { ok: true; scope: NetlistScopeKind; netlist: SchematicNetlist }
|
|
49
|
+
| { ok: false; scope: NetlistScopeKind; error: { kind: string; message: string } }
|
|
50
|
+
|
|
51
|
+
async function runScope(
|
|
52
|
+
env: NetlistToolEnv,
|
|
53
|
+
scope: NetlistScopeKind,
|
|
54
|
+
): Promise<ScopeResult> {
|
|
55
|
+
try {
|
|
56
|
+
let netlist: SchematicNetlist
|
|
57
|
+
if (scope === 'project') netlist = await env.client.getProjectNetlist()
|
|
58
|
+
else if (scope === 'selection') netlist = await env.client.getSelectionNetlist()
|
|
59
|
+
else netlist = await env.client.getActivePageNetlist()
|
|
60
|
+
|
|
61
|
+
return { ok: true, scope, netlist }
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const kind =
|
|
64
|
+
err instanceof NetlistError
|
|
65
|
+
? err.kind
|
|
66
|
+
: ('INTERNAL' as const)
|
|
67
|
+
const message = String((err as Error)?.message ?? err)
|
|
68
|
+
return { ok: false, scope, error: { kind, message } }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function scopeDescription(scope: NetlistScopeKind, extra: string): string {
|
|
73
|
+
return (
|
|
74
|
+
`Read the current schematic netlist from the EDA host through hq-edge ` +
|
|
75
|
+
`(DSH → dsh-eda-host → hq-edge → EDA host). ` +
|
|
76
|
+
extra +
|
|
77
|
+
` The result is a semantic netlist JSON: { ok, scope, netlist: { components[], nets[] } }. ` +
|
|
78
|
+
`Each component has referenceDesignators[], value, manufacturerPartNumber, footprint, ` +
|
|
79
|
+
`description and pins[] (pinNumber, pinName, electricalType); each net has name and ` +
|
|
80
|
+
`pinReferences[] (referenceDesignator, pinNumber). ` +
|
|
81
|
+
`IMPORTANT SEMANTICS: ok:true with empty components/nets is a VALID empty design — do not ` +
|
|
82
|
+
`treat it as a failure. On ok:false, error.kind distinguishes the cause: ` +
|
|
83
|
+
`"FAILED_PRECONDITION" (no EDA host / no live editor — ask the user to open the design in ` +
|
|
84
|
+
`the EDA editor first, then retry), "UNIMPLEMENTED" (this scope is not supported by the ` +
|
|
85
|
+
`current host — do NOT retry; report it to the user), "UNAVAILABLE" (hq-edge host unreachable), ` +
|
|
86
|
+
`"INTERNAL" (host-side failure). Do NOT fabricate netlist data.`
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createNetListTools(env: NetlistToolEnv) {
|
|
91
|
+
const mkTool = (scope: NetlistScopeKind, name: string, desc: string) =>
|
|
92
|
+
defineTool({
|
|
93
|
+
name,
|
|
94
|
+
description: desc,
|
|
95
|
+
parameters: {},
|
|
96
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
97
|
+
async execute(_args: unknown, _exec: ToolExecLike) {
|
|
98
|
+
return asJson(await runScope(env, scope))
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return [
|
|
103
|
+
mkTool(
|
|
104
|
+
'project',
|
|
105
|
+
'get_project_netlist',
|
|
106
|
+
scopeDescription(
|
|
107
|
+
'project',
|
|
108
|
+
'Returns the complete logical netlist for the current project (all sheets, all nets).',
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
mkTool(
|
|
112
|
+
'selection',
|
|
113
|
+
'get_selection_netlist',
|
|
114
|
+
scopeDescription(
|
|
115
|
+
'selection',
|
|
116
|
+
'Returns the netlist associated with the currently selected schematic components, ' +
|
|
117
|
+
'including the nets they participate in (each net lists every connected pin, not ' +
|
|
118
|
+
'only the selected ones).',
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
mkTool(
|
|
122
|
+
'active_page',
|
|
123
|
+
'get_active_page_netlist',
|
|
124
|
+
scopeDescription(
|
|
125
|
+
'active_page',
|
|
126
|
+
'Returns the netlist for the currently active schematic page. NOTE: KiCad host does ' +
|
|
127
|
+
'not implement this scope — expect ok:false with error.kind "UNIMPLEMENTED".',
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
]
|
|
131
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic netlist types for `@huaqiu/dsh-eda-host`.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained structural mirror of `hq.ir.schematic.v1` (the hq-edge-owned
|
|
5
|
+
* semantic protobuf contract). The plugin intentionally does NOT depend on
|
|
6
|
+
* `@hqedge/*` — this file is the only place the wire shapes are named, so
|
|
7
|
+
* keeping them aligned with the proto is a one-file concern.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Electrical type of a pin — mirrors `hq.ir.logical.v1.ElectricalType`. */
|
|
13
|
+
export type ElectricalType =
|
|
14
|
+
| 'ELECTRICAL_TYPE_UNSPECIFIED'
|
|
15
|
+
| 'ELECTRICAL_TYPE_INPUT'
|
|
16
|
+
| 'ELECTRICAL_TYPE_OUTPUT'
|
|
17
|
+
| 'ELECTRICAL_TYPE_BIDIRECTIONAL'
|
|
18
|
+
| 'ELECTRICAL_TYPE_TRISTATE'
|
|
19
|
+
| 'ELECTRICAL_TYPE_PASSIVE'
|
|
20
|
+
| 'ELECTRICAL_TYPE_POWER_IN'
|
|
21
|
+
| 'ELECTRICAL_TYPE_POWER_OUT'
|
|
22
|
+
| 'ELECTRICAL_TYPE_OPEN_COLLECTOR'
|
|
23
|
+
| 'ELECTRICAL_TYPE_OPEN_EMITTER'
|
|
24
|
+
| 'ELECTRICAL_TYPE_NO_CONNECT'
|
|
25
|
+
|
|
26
|
+
export interface PinDefinition {
|
|
27
|
+
pinNumber: string
|
|
28
|
+
pinName: string
|
|
29
|
+
electricalType: ElectricalType
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SchematicComponent {
|
|
33
|
+
referenceDesignators: string[]
|
|
34
|
+
value: string
|
|
35
|
+
manufacturerPartNumber: string
|
|
36
|
+
footprint: string
|
|
37
|
+
description: string
|
|
38
|
+
pins: PinDefinition[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PinReference {
|
|
42
|
+
referenceDesignator: string
|
|
43
|
+
pinNumber: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ElectricalNet {
|
|
47
|
+
name: string
|
|
48
|
+
portName?: string
|
|
49
|
+
pinReferences: PinReference[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SchematicNetlist {
|
|
53
|
+
components: SchematicComponent[]
|
|
54
|
+
nets: ElectricalNet[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Semantic error categories for netlist retrieval. Mirrors the gRPC status
|
|
59
|
+
* contract of `hq.ir.schematic.v1.NetListService` so an agent can distinguish
|
|
60
|
+
* "valid but empty" from "cannot answer at all".
|
|
61
|
+
*/
|
|
62
|
+
export type NetlistErrorKind =
|
|
63
|
+
/** No EDA host is reachable (hq-edge not configured / host down). */
|
|
64
|
+
| 'FAILED_PRECONDITION'
|
|
65
|
+
/** The host does not implement this scope (e.g. active page on KiCad). */
|
|
66
|
+
| 'UNIMPLEMENTED'
|
|
67
|
+
/** Host-side runtime failure. */
|
|
68
|
+
| 'INTERNAL'
|
|
69
|
+
/** Host unavailable (connection refused). */
|
|
70
|
+
| 'UNAVAILABLE'
|
|
71
|
+
|
|
72
|
+
export class NetlistError extends Error {
|
|
73
|
+
readonly kind: NetlistErrorKind
|
|
74
|
+
|
|
75
|
+
constructor(kind: NetlistErrorKind, message: string) {
|
|
76
|
+
super(message)
|
|
77
|
+
this.name = 'NetlistError'
|
|
78
|
+
this.kind = kind
|
|
79
|
+
}
|
|
80
|
+
}
|