@huaqiu/dsh-tool-schematic-gen 0.3.7 → 0.3.9
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/client.js +166 -6
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +31 -15
- package/package.json +6 -6
- package/src/client/hit-card.tsx +78 -0
- package/src/client/i18n.ts +10 -0
- package/src/client/index.ts +8 -0
- package/src/client/parse.ts +22 -3
- package/src/client/place.ts +17 -0
- package/src/tools.ts +43 -7
package/lib/index.mjs
CHANGED
|
@@ -787,6 +787,33 @@ async function createPreviewArtifact(env, type, filename, content, contentEncodi
|
|
|
787
787
|
});
|
|
788
788
|
}
|
|
789
789
|
/**
|
|
790
|
+
* Best-effort cross-process URI for a stored artifact (`file://` to its
|
|
791
|
+
* content on disk). This is what the Place action hands to HQ Edge — an
|
|
792
|
+
* artifact *id* is store-local and meaningless there. Never throws: a
|
|
793
|
+
* generation must not fail because the URI is unavailable; the card simply
|
|
794
|
+
* hides Place when there is no uri.
|
|
795
|
+
*/
|
|
796
|
+
async function artifactUriOf(env, id) {
|
|
797
|
+
try {
|
|
798
|
+
if (!env.artifacts || typeof env.artifacts.getDownloadUri !== "function") return null;
|
|
799
|
+
return await env.artifacts.getDownloadUri(id);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
console.warn(LOG_TAG$1, "getDownloadUri failed for", id, String(err?.message || err));
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
async function toArtifactEntry(env, created) {
|
|
806
|
+
const entry = {
|
|
807
|
+
id: created.id,
|
|
808
|
+
type: created.type,
|
|
809
|
+
filename: created.filename,
|
|
810
|
+
size: created.size
|
|
811
|
+
};
|
|
812
|
+
const uri = await artifactUriOf(env, created.id);
|
|
813
|
+
if (uri) entry.uri = uri;
|
|
814
|
+
return entry;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
790
817
|
* Pull the schematic deliverable out of the final agent state. `schFiles`
|
|
791
818
|
* carry the `.kicad_sch` content inline, so the text is returned verbatim.
|
|
792
819
|
*/
|
|
@@ -826,12 +853,7 @@ async function materializeSchematicArtifacts(env, schFiles) {
|
|
|
826
853
|
const file = schFiles[i];
|
|
827
854
|
try {
|
|
828
855
|
const created = await createPreviewArtifact(env, "schematic", file.filename, file.content);
|
|
829
|
-
artifacts.push(
|
|
830
|
-
id: created.id,
|
|
831
|
-
type: created.type,
|
|
832
|
-
filename: created.filename,
|
|
833
|
-
size: created.size
|
|
834
|
-
});
|
|
856
|
+
artifacts.push(await toArtifactEntry(env, created));
|
|
835
857
|
} catch (storeErr) {
|
|
836
858
|
anyFailed = true;
|
|
837
859
|
outFiles[i].content = file.content;
|
|
@@ -979,13 +1001,7 @@ async function runGenerateSystem(args, exec, env) {
|
|
|
979
1001
|
const notes = [];
|
|
980
1002
|
let zipArtifact = null;
|
|
981
1003
|
try {
|
|
982
|
-
|
|
983
|
-
zipArtifact = {
|
|
984
|
-
id: created.id,
|
|
985
|
-
type: created.type,
|
|
986
|
-
filename: created.filename,
|
|
987
|
-
size: created.size
|
|
988
|
-
};
|
|
1004
|
+
zipArtifact = await toArtifactEntry(env, await createPreviewArtifact(env, "zip", sanitizeZipBaseName(designName) + ".zip", zipBuf.toString("base64"), "base64"));
|
|
989
1005
|
} catch (storeErr) {
|
|
990
1006
|
notes.push("Could not store the project zip as an artifact (" + String(storeErr?.message || storeErr) + "); kept it in the result instead.");
|
|
991
1007
|
}
|
|
@@ -1015,7 +1031,7 @@ const AUTH_GATE_NOTE = "AUTH: This tool requires a Huaqiu EDA (eda.cn) account.
|
|
|
1015
1031
|
function createSchematicTool(env) {
|
|
1016
1032
|
return defineTool({
|
|
1017
1033
|
name: "generate_schematic_from_description",
|
|
1018
|
-
description: "Generate a KiCad schematic (.kicad_sch files) from a natural-language description of a circuit or sub-circuit — e.g. \"design a 5V LM7805 linear regulator power supply with input and output filter capacitors\". Calls the online HQ-EDA schematic generation agent and returns schFiles (filename references), schArtifacts (preview artifact references with id/type/filename/size
|
|
1034
|
+
description: "Generate a KiCad schematic (.kicad_sch files) from a natural-language description of a circuit or sub-circuit — e.g. \"design a 5V LM7805 linear regulator power supply with input and output filter capacitors\". Calls the online HQ-EDA schematic generation agent and returns schFiles (filename references), schArtifacts (preview artifact references with id/type/filename/size plus a uri when the cross-process placement channel is available), kicadPro and project_achieve_url. Use this when the user asks to draw, generate or create a circuit schematic from a description (not from an image — for that use the symbol/footprint tools). IMPORTANT: The generated schematic renders automatically as a result card in the web client — an interactive canvas preview per sheet (multi-sheet results get a sheet tab bar) and a download button for the current sheet. Do NOT paste the schematic source, file URLs, or any fenced code block into your reply; just note in one line that the schematic was generated and how many sheets it has. " + AUTH_GATE_NOTE,
|
|
1019
1035
|
parameters: {
|
|
1020
1036
|
description: {
|
|
1021
1037
|
type: "string",
|
|
@@ -1040,7 +1056,7 @@ function createSchematicTool(env) {
|
|
|
1040
1056
|
function createSystemTool(env) {
|
|
1041
1057
|
return defineTool({
|
|
1042
1058
|
name: "generate_system_module_graph",
|
|
1043
|
-
description: "Generate a hardware system design (module graph) from a natural-language description — e.g. \"design a small smart alarm clock\". Calls the online HQ-EDA system-design agent, which plans the modules, searches/selects parts, wires the connections, and produces a module graph; the graph is then exported to a KiCad project zip. Returns: a zipArtifact reference (preview-artifact id of the full project zip — the zip is never inlined into the conversation) and a summary (design name, module count, connection count, module names). Use this when the user wants a whole system/module-level design, not a single schematic or symbol. IMPORTANT: The generated system design renders automatically as a result card in the web client — a canvas preview of the project root schematic (fetched from the zip artifact) and a Download button for the full project zip. Do NOT paste the schematic source, file URLs, or any fenced code block into your reply; just note in one line that the design was generated, its module count, and that the project zip is downloadable from the card. " + AUTH_GATE_NOTE,
|
|
1059
|
+
description: "Generate a hardware system design (module graph) from a natural-language description — e.g. \"design a small smart alarm clock\". Calls the online HQ-EDA system-design agent, which plans the modules, searches/selects parts, wires the connections, and produces a module graph; the graph is then exported to a KiCad project zip. Returns: a zipArtifact reference (preview-artifact id of the full project zip — the zip is never inlined into the conversation; a uri field is included when the cross-process placement channel is available) and a summary (design name, module count, connection count, module names). Use this when the user wants a whole system/module-level design, not a single schematic or symbol. IMPORTANT: The generated system design renders automatically as a result card in the web client — a canvas preview of the project root schematic (fetched from the zip artifact) and a Download button for the full project zip. Do NOT paste the schematic source, file URLs, or any fenced code block into your reply; just note in one line that the design was generated, its module count, and that the project zip is downloadable from the card. " + AUTH_GATE_NOTE,
|
|
1044
1060
|
parameters: {
|
|
1045
1061
|
description: {
|
|
1046
1062
|
type: "string",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-tool-schematic-gen",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.mjs",
|
|
6
6
|
"types": "./lib/index.d.mts",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
31
31
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.0",
|
|
32
32
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.0",
|
|
33
|
-
"@huaqiu/dsh-auth": "^0.3.
|
|
34
|
-
"@huaqiu/dsh-artifacts": "^0.3.
|
|
33
|
+
"@huaqiu/dsh-auth": "^0.3.9",
|
|
34
|
+
"@huaqiu/dsh-artifacts": "^0.3.9",
|
|
35
35
|
"react": "^18"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"@huaqiu/kicad-sexpr-parser": "^0.1.1",
|
|
40
40
|
"@types/react": "^18.3.31",
|
|
41
41
|
"react": "^18.3.1",
|
|
42
|
-
"@huaqiu/dsh-
|
|
43
|
-
"@huaqiu/dsh-auth": "0.3.
|
|
44
|
-
"@huaqiu/dsh-
|
|
42
|
+
"@huaqiu/dsh-artifacts": "0.3.9",
|
|
43
|
+
"@huaqiu/dsh-auth": "0.3.9",
|
|
44
|
+
"@huaqiu/dsh-tool-uncollapse": "0.3.9"
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
47
|
"lib",
|
package/src/client/hit-card.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
resolveArtifactText, resolveArtifactBytes, renderSheetToCanvas,
|
|
23
23
|
renderProjectZipToCanvas, sizeCanvasFor, downloadText, downloadBytes,
|
|
24
24
|
} from './ecad.js'
|
|
25
|
+
import { placeSupportOf, type HqEdgePlaceLike } from './place.js'
|
|
25
26
|
import { useLocale, useTheme } from './theme.js'
|
|
26
27
|
import { buildLoginUrl, loginIframeBackground } from './login-url.js'
|
|
27
28
|
import { LiveProgress } from './stack-frame.jsx'
|
|
@@ -52,6 +53,13 @@ export interface GenHitProps {
|
|
|
52
53
|
inspect?: () => void
|
|
53
54
|
authState?: AuthStateLike
|
|
54
55
|
sendPrompt?: PromptSender
|
|
56
|
+
/**
|
|
57
|
+
* Lazy accessor for the host's `hqEdge` service (edge-bridge browser half).
|
|
58
|
+
* Absent/undefined in standalone DSH — the Place button is then hidden.
|
|
59
|
+
* Lazy (not captured once) so plugin load order and HQ Edge restarts both
|
|
60
|
+
* resolve correctly at click time.
|
|
61
|
+
*/
|
|
62
|
+
getHqEdge?: () => HqEdgePlaceLike | undefined
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
function kindOf(toolName: string): 'schematic' | 'system' {
|
|
@@ -267,6 +275,9 @@ export const GenHit = memo(function GenHit(props: GenHitProps): ReactElement {
|
|
|
267
275
|
}, [artifactKey, state.phase])
|
|
268
276
|
|
|
269
277
|
const [busy, setBusy] = useState<string | null>(null)
|
|
278
|
+
// Outcome of the last Place attempt: 'ok' | 'error:<detail>'. Rendered as a
|
|
279
|
+
// one-line status under the actions so the user knows the placement landed.
|
|
280
|
+
const [placeStatus, setPlaceStatus] = useState<string | null>(null)
|
|
270
281
|
|
|
271
282
|
function onDownload(): void {
|
|
272
283
|
if (busy || payload.phase !== 'ready') return
|
|
@@ -298,6 +309,59 @@ export const GenHit = memo(function GenHit(props: GenHitProps): ReactElement {
|
|
|
298
309
|
)
|
|
299
310
|
}
|
|
300
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Place the generated design into the host EDA editor via HQ Edge.
|
|
314
|
+
*
|
|
315
|
+
* A system design sends ONE request (the project zip; HQ Edge derives the
|
|
316
|
+
* root from the `*.kicad_pro` name — the same convention as the preview).
|
|
317
|
+
* A multi-sheet schematic sends one request per sheet so every sheet lands
|
|
318
|
+
* in the editor. Failures are collected and reported as a one-line status;
|
|
319
|
+
* a partial success is explicitly surfaced, not swallowed.
|
|
320
|
+
*/
|
|
321
|
+
function placeRequestsOf(): Array<{ artifactUri: string; filename: string | null }> {
|
|
322
|
+
if (!result) return []
|
|
323
|
+
// System kind: exactly one zip artifact (one request; HQ Edge derives the
|
|
324
|
+
// root from the `*.kicad_pro` name inside the zip). Schematic kind: one
|
|
325
|
+
// request per sheet. The wire shape is identical either way.
|
|
326
|
+
return result.artifacts
|
|
327
|
+
.filter((a) => a.uri)
|
|
328
|
+
.map((a) => ({ artifactUri: a.uri!, filename: a.filename }))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function onPlace(): Promise<void> {
|
|
332
|
+
if (busy) return
|
|
333
|
+
const support = placeSupportOf(props.getHqEdge, 'schematic')?.()
|
|
334
|
+
if (!support) return
|
|
335
|
+
const requests = placeRequestsOf()
|
|
336
|
+
if (requests.length === 0) return
|
|
337
|
+
setBusy('place')
|
|
338
|
+
setPlaceStatus(null)
|
|
339
|
+
let placed = 0
|
|
340
|
+
const failures: string[] = []
|
|
341
|
+
for (const req of requests) {
|
|
342
|
+
try {
|
|
343
|
+
await support.place({
|
|
344
|
+
type: 'schematic',
|
|
345
|
+
artifactUri: req.artifactUri,
|
|
346
|
+
...(req.filename ? { filename: req.filename } : {}),
|
|
347
|
+
})
|
|
348
|
+
placed++
|
|
349
|
+
} catch (err) {
|
|
350
|
+
const detail = String((err as Error)?.message || err)
|
|
351
|
+
console.warn('[hq-schematic-gen] place failed', detail)
|
|
352
|
+
failures.push(detail)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
setBusy(null)
|
|
356
|
+
if (failures.length === 0) {
|
|
357
|
+
setPlaceStatus(t('card.place.done', { count: placed }))
|
|
358
|
+
} else if (placed > 0) {
|
|
359
|
+
setPlaceStatus(t('card.place.partial', { placed, failed: failures.length }) + ' ' + failures[0])
|
|
360
|
+
} else {
|
|
361
|
+
setPlaceStatus(t('card.place.failed') + ' ' + failures[0])
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
301
365
|
// needs_auth
|
|
302
366
|
if (state.phase === 'needs_auth') {
|
|
303
367
|
return <LoginCard toolName={props.toolName} authState={props.authState} t={t} />
|
|
@@ -355,6 +419,12 @@ export const GenHit = memo(function GenHit(props: GenHitProps): ReactElement {
|
|
|
355
419
|
}
|
|
356
420
|
|
|
357
421
|
const canDownload = payload.phase === 'ready' && (payload.source != null || payload.bytes != null)
|
|
422
|
+
// Place is offered only when the host provides hqEdge, the current editor
|
|
423
|
+
// accepts schematics (frontend matrix; HQ Edge re-enforces server-side) and
|
|
424
|
+
// the node half resolved at least one artifact uri.
|
|
425
|
+
const placeSupport = placeSupportOf(props.getHqEdge, 'schematic')
|
|
426
|
+
const placeableCount = result.artifacts.filter((a) => a.uri).length
|
|
427
|
+
const canPlace = !!placeSupport?.() && placeableCount > 0
|
|
358
428
|
|
|
359
429
|
return (
|
|
360
430
|
<div className="hq-sch">
|
|
@@ -366,6 +436,13 @@ export const GenHit = memo(function GenHit(props: GenHitProps): ReactElement {
|
|
|
366
436
|
<button type="button" className="hq-sch__act" onClick={onDownload} disabled={!canDownload || busy === 'download'}>
|
|
367
437
|
⭳ {busy === 'download' ? t('card.action.downloading') : t('card.action.download')}
|
|
368
438
|
</button>
|
|
439
|
+
{canPlace
|
|
440
|
+
? (
|
|
441
|
+
<button type="button" className="hq-sch__act" onClick={onPlace} disabled={busy === 'place'}>
|
|
442
|
+
⇥ {busy === 'place' ? t('card.action.placing') : t('card.action.place')}
|
|
443
|
+
</button>
|
|
444
|
+
)
|
|
445
|
+
: null}
|
|
369
446
|
<button type="button" className="hq-sch__act" onClick={onRegenerate} disabled={busy === 'regenerate'}>
|
|
370
447
|
↻ {busy === 'regenerate' ? t('card.action.regenerating') : t('card.action.regenerate')}
|
|
371
448
|
</button>
|
|
@@ -373,6 +450,7 @@ export const GenHit = memo(function GenHit(props: GenHitProps): ReactElement {
|
|
|
373
450
|
? <button type="button" className="hq-sch__act" onClick={() => props.inspect?.()}>{t('card.action.inspect')}</button>
|
|
374
451
|
: null}
|
|
375
452
|
</div>
|
|
453
|
+
{placeStatus ? <div className="hq-sch__note">{placeStatus}</div> : null}
|
|
376
454
|
</div>
|
|
377
455
|
)
|
|
378
456
|
})
|
package/src/client/i18n.ts
CHANGED
|
@@ -45,6 +45,11 @@ const ZH = {
|
|
|
45
45
|
'card.meta.connections': '{count} 条连接',
|
|
46
46
|
'card.action.download': '下载',
|
|
47
47
|
'card.action.downloading': '下载中…',
|
|
48
|
+
'card.action.place': '放置到编辑器',
|
|
49
|
+
'card.action.placing': '放置中…',
|
|
50
|
+
'card.place.done': '已放置 {count} 个文件到编辑器',
|
|
51
|
+
'card.place.partial': '已放置 {placed} 个,{failed} 个失败:',
|
|
52
|
+
'card.place.failed': '放置失败:',
|
|
48
53
|
'card.action.regenerate': '重新生成',
|
|
49
54
|
'card.action.regenerating': '重新生成中…',
|
|
50
55
|
'card.action.inspect': '详情',
|
|
@@ -95,6 +100,11 @@ const EN: Record<CopyKey, string> = {
|
|
|
95
100
|
'card.meta.connections': '{count} connection(s)',
|
|
96
101
|
'card.action.download': 'Download',
|
|
97
102
|
'card.action.downloading': 'Downloading…',
|
|
103
|
+
'card.action.place': 'Place in editor',
|
|
104
|
+
'card.action.placing': 'Placing…',
|
|
105
|
+
'card.place.done': 'Placed {count} file(s) into the editor',
|
|
106
|
+
'card.place.partial': 'Placed {placed}, {failed} failed: ',
|
|
107
|
+
'card.place.failed': 'Placement failed: ',
|
|
98
108
|
'card.action.regenerate': 'Regenerate',
|
|
99
109
|
'card.action.regenerating': 'Regenerating…',
|
|
100
110
|
'card.action.inspect': 'Inspect',
|
package/src/client/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { createElement, useEffect, useState, type ComponentType } from 'react'
|
|
|
17
17
|
import { GenHit } from './hit-card.jsx'
|
|
18
18
|
import { injectStyles, removeStyles, disposeThemeObserver, installSchematicUncollapser } from './theme.js'
|
|
19
19
|
import type { AuthStateLike, PromptSender } from './hit-card.jsx'
|
|
20
|
+
import type { HqEdgePlaceLike } from './place.js'
|
|
20
21
|
|
|
21
22
|
export type { AuthStateLike, PromptSender }
|
|
22
23
|
|
|
@@ -116,6 +117,12 @@ export function apply(ctx: ClientContext): () => void {
|
|
|
116
117
|
const authService = ctx.get<HuaqiuAuthClientService | undefined>('huaqiuAuth')
|
|
117
118
|
const useAuthState = createUseAuthState(authService?.auth)
|
|
118
119
|
|
|
120
|
+
// Lazy `hqEdge` accessor for the Place action. NOT captured once: the
|
|
121
|
+
// edge-bridge plugin may load after this one, and a click should see the
|
|
122
|
+
// service as soon as it exists. Undefined in standalone DSH (no edge-bridge)
|
|
123
|
+
// — the card then simply hides Place.
|
|
124
|
+
const getHqEdge = () => ctx.get<HqEdgePlaceLike | undefined>('hqEdge')
|
|
125
|
+
|
|
119
126
|
const sendPrompt: PromptSender = (sessionId, message) => {
|
|
120
127
|
const sessions = ctx.get<{ binding(id: string): { session: { prompt(content: Array<{ type: 'text'; text: string }>, mode: 'queue'): Promise<unknown> } } | undefined }>('sessions')
|
|
121
128
|
if (!sessions || typeof sessions.binding !== 'function' || !sessionId) {
|
|
@@ -151,6 +158,7 @@ export function apply(ctx: ClientContext): () => void {
|
|
|
151
158
|
callId: props.callId,
|
|
152
159
|
sendPrompt,
|
|
153
160
|
authState: useAuthState(),
|
|
161
|
+
getHqEdge,
|
|
154
162
|
})
|
|
155
163
|
|
|
156
164
|
for (const toolName of TOOLVIEW_KEYS) {
|
package/src/client/parse.ts
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* Result shapes produced by the node half (`src/tools.ts`):
|
|
5
5
|
* generate_schematic_from_description →
|
|
6
6
|
* { status:'generated', kind:'schematic', design_name, schFiles:[{filename}],
|
|
7
|
-
* schArtifacts:[{id,type,filename,size}], kicadPro, project_achieve_url, note? }
|
|
7
|
+
* schArtifacts:[{id,type,filename,size,uri?}], kicadPro, project_achieve_url, note? }
|
|
8
8
|
* generate_system_module_graph →
|
|
9
9
|
* { status:'generated', kind:'system', design_name, module_count,
|
|
10
10
|
* connection_count, module_names, zip_bytes,
|
|
11
|
-
* zipArtifact:{id,type:'zip',filename,size}, note? }
|
|
11
|
+
* zipArtifact:{id,type:'zip',filename,size,uri?}, note? }
|
|
12
12
|
* both may return { status:'needs_auth', kind, hint }
|
|
13
|
+
*
|
|
14
|
+
* `uri` (when present) is the HQ Edge-resolvable handle the Place action uses.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
export interface ContentBlockLike {
|
|
@@ -28,12 +30,21 @@ export interface ArtifactRef {
|
|
|
28
30
|
type: string | null
|
|
29
31
|
filename: string | null
|
|
30
32
|
size: number | null
|
|
33
|
+
/**
|
|
34
|
+
* HQ Edge-resolvable URI (`file://`) for the artifact bytes — the handle the
|
|
35
|
+
* Place action passes to `hqEdge.placeArtifact`. `null` when the node half
|
|
36
|
+
* could not resolve it (standalone DSH, older node half): the card then
|
|
37
|
+
* hides Place.
|
|
38
|
+
*/
|
|
39
|
+
uri: string | null
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
export interface SchResult {
|
|
34
43
|
status: string | null
|
|
35
44
|
kind: string | null
|
|
36
45
|
artifact: ArtifactRef | null
|
|
46
|
+
/** Every placeable artifact (schematic sheets / the project zip). */
|
|
47
|
+
artifacts: ArtifactRef[]
|
|
37
48
|
designName: string | null
|
|
38
49
|
fileCount: number | null
|
|
39
50
|
moduleCount: number | null
|
|
@@ -79,6 +90,7 @@ function artOf(a: unknown): ArtifactRef | null {
|
|
|
79
90
|
type: typeof o.type === 'string' ? o.type : null,
|
|
80
91
|
filename: typeof o.filename === 'string' ? o.filename : null,
|
|
81
92
|
size: typeof o.size === 'number' ? o.size : null,
|
|
93
|
+
uri: typeof o.uri === 'string' && o.uri.length > 0 ? o.uri : null,
|
|
82
94
|
}
|
|
83
95
|
}
|
|
84
96
|
|
|
@@ -98,17 +110,24 @@ export function parseSchResult(text: string): SchResult | null {
|
|
|
98
110
|
const kind = typeof o.kind === 'string' ? o.kind : null
|
|
99
111
|
|
|
100
112
|
let artifact: ArtifactRef | null = null
|
|
113
|
+
const artifacts: ArtifactRef[] = []
|
|
101
114
|
if (kind === 'system') {
|
|
102
115
|
artifact = artOf(o.zipArtifact)
|
|
116
|
+
if (artifact) artifacts.push(artifact)
|
|
103
117
|
} else {
|
|
104
118
|
const list = Array.isArray(o.schArtifacts) ? (o.schArtifacts as unknown[]) : []
|
|
105
|
-
|
|
119
|
+
for (const entry of list) {
|
|
120
|
+
const ref = artOf(entry)
|
|
121
|
+
if (ref) artifacts.push(ref)
|
|
122
|
+
}
|
|
123
|
+
artifact = artifacts.length > 0 ? artifacts[0]! : null
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
return {
|
|
109
127
|
status,
|
|
110
128
|
kind,
|
|
111
129
|
artifact,
|
|
130
|
+
artifacts,
|
|
112
131
|
designName: typeof o.design_name === 'string' && o.design_name ? o.design_name : null,
|
|
113
132
|
fileCount: Array.isArray(o.schFiles) ? (o.schFiles as unknown[]).length
|
|
114
133
|
: (Array.isArray(o.schArtifacts) ? (o.schArtifacts as unknown[]).length : null),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Place-action seam for the schematic/system HIT card.
|
|
3
|
+
*
|
|
4
|
+
* Thin re-export of the shared, browser-safe placement module
|
|
5
|
+
* (`@huaqiu/dsh-artifacts/placement`): the frontend compatibility matrix plus
|
|
6
|
+
* the lazy `hqEdge` service accessor. Kept as a local module so the card's
|
|
7
|
+
* import paths stay stable and the client bundle only ever pulls the pure
|
|
8
|
+
* subpath — the package root exports the cordis plugin entry, which must not
|
|
9
|
+
* be bundled into the browser.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
export {
|
|
14
|
+
placeSupportOf,
|
|
15
|
+
type HqEdgePlaceLike,
|
|
16
|
+
type PlaceSupport,
|
|
17
|
+
} from '@huaqiu/dsh-artifacts/placement'
|
package/src/tools.ts
CHANGED
|
@@ -166,6 +166,40 @@ async function createPreviewArtifact(
|
|
|
166
166
|
return env.artifacts.create({ type, filename, content, contentEncoding })
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Best-effort cross-process URI for a stored artifact (`file://` to its
|
|
171
|
+
* content on disk). This is what the Place action hands to HQ Edge — an
|
|
172
|
+
* artifact *id* is store-local and meaningless there. Never throws: a
|
|
173
|
+
* generation must not fail because the URI is unavailable; the card simply
|
|
174
|
+
* hides Place when there is no uri.
|
|
175
|
+
*/
|
|
176
|
+
async function artifactUriOf(env: SchematicGenEnv, id: string): Promise<string | null> {
|
|
177
|
+
try {
|
|
178
|
+
if (!env.artifacts || typeof env.artifacts.getDownloadUri !== 'function') return null
|
|
179
|
+
return await env.artifacts.getDownloadUri(id)
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.warn(LOG_TAG, 'getDownloadUri failed for', id, String((err as Error)?.message || err))
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Artifact entry shape in the tool result (parsed by the client card). */
|
|
187
|
+
interface ArtifactEntry {
|
|
188
|
+
id: string
|
|
189
|
+
type: string
|
|
190
|
+
filename: string
|
|
191
|
+
size: number
|
|
192
|
+
/** HQ Edge-resolvable URI (`file://`), present when resolvable. */
|
|
193
|
+
uri?: string
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function toArtifactEntry(env: SchematicGenEnv, created: CreateArtifactResult): Promise<ArtifactEntry> {
|
|
197
|
+
const entry: ArtifactEntry = { id: created.id, type: created.type, filename: created.filename, size: created.size }
|
|
198
|
+
const uri = await artifactUriOf(env, created.id)
|
|
199
|
+
if (uri) entry.uri = uri
|
|
200
|
+
return entry
|
|
201
|
+
}
|
|
202
|
+
|
|
169
203
|
// ── Deliverable extraction ───────────────────────────────────────────────────
|
|
170
204
|
|
|
171
205
|
export interface SchematicSheet {
|
|
@@ -219,7 +253,7 @@ export function extractModuleGraph(state: Record<string, unknown>): Record<strin
|
|
|
219
253
|
|
|
220
254
|
interface MaterializedSchematic {
|
|
221
255
|
schFiles: Array<{ filename: string; content?: string }>
|
|
222
|
-
schArtifacts?: Array<
|
|
256
|
+
schArtifacts?: Array<ArtifactEntry>
|
|
223
257
|
/** User-safe status detail — the client card renders this. */
|
|
224
258
|
note?: string
|
|
225
259
|
/** Agent-only explanation/directive — the client card MUST NOT render it. */
|
|
@@ -234,7 +268,7 @@ interface MaterializedSchematic {
|
|
|
234
268
|
*/
|
|
235
269
|
async function materializeSchematicArtifacts(env: SchematicGenEnv, schFiles: SchematicSheet[]): Promise<MaterializedSchematic> {
|
|
236
270
|
const outFiles: Array<{ filename: string; content?: string }> = schFiles.map((f) => ({ filename: f.filename }))
|
|
237
|
-
const artifacts:
|
|
271
|
+
const artifacts: ArtifactEntry[] = []
|
|
238
272
|
let anyFailed = false
|
|
239
273
|
let errorNote = ''
|
|
240
274
|
|
|
@@ -242,7 +276,7 @@ async function materializeSchematicArtifacts(env: SchematicGenEnv, schFiles: Sch
|
|
|
242
276
|
const file = schFiles[i]!
|
|
243
277
|
try {
|
|
244
278
|
const created = await createPreviewArtifact(env, 'schematic', file.filename, file.content)
|
|
245
|
-
artifacts.push(
|
|
279
|
+
artifacts.push(await toArtifactEntry(env, created))
|
|
246
280
|
} catch (storeErr) {
|
|
247
281
|
anyFailed = true
|
|
248
282
|
outFiles[i]!.content = file.content // data-loss guard
|
|
@@ -445,11 +479,11 @@ export async function runGenerateSystem(
|
|
|
445
479
|
// Store the project zip as a `zip` preview artifact (primary). Keeping the
|
|
446
480
|
// zip OUT of the JSON keeps the result small — inlining base64 used to
|
|
447
481
|
// truncate the tool result and fail the card.
|
|
448
|
-
let zipArtifact:
|
|
482
|
+
let zipArtifact: ArtifactEntry | null = null
|
|
449
483
|
try {
|
|
450
484
|
const safeName = sanitizeZipBaseName(designName)
|
|
451
485
|
const created = await createPreviewArtifact(env, 'zip', safeName + '.zip', zipBuf.toString('base64'), 'base64')
|
|
452
|
-
zipArtifact =
|
|
486
|
+
zipArtifact = await toArtifactEntry(env, created)
|
|
453
487
|
} catch (storeErr) {
|
|
454
488
|
notes.push('Could not store the project zip as an artifact (' +
|
|
455
489
|
String((storeErr as Error)?.message || storeErr) + '); kept it in the result instead.')
|
|
@@ -507,7 +541,8 @@ function createSchematicTool(env: SchematicGenEnv) {
|
|
|
507
541
|
'regulator power supply with input and output filter capacitors". Calls the ' +
|
|
508
542
|
'online HQ-EDA schematic generation agent and returns ' +
|
|
509
543
|
'schFiles (filename references), schArtifacts (preview artifact references ' +
|
|
510
|
-
'with id/type/filename/size
|
|
544
|
+
'with id/type/filename/size plus a uri when the cross-process placement ' +
|
|
545
|
+
'channel is available), kicadPro and project_achieve_url. ' +
|
|
511
546
|
'Use this when the user asks to draw, generate or create a circuit ' +
|
|
512
547
|
'schematic from a description (not from an image — for that use the ' +
|
|
513
548
|
'symbol/footprint tools). ' +
|
|
@@ -549,7 +584,8 @@ function createSystemTool(env: SchematicGenEnv) {
|
|
|
549
584
|
'parts, wires the connections, and produces a module graph; the graph is ' +
|
|
550
585
|
'then exported to a KiCad project zip. Returns: a zipArtifact reference ' +
|
|
551
586
|
'(preview-artifact id of the full project zip — the zip is never inlined ' +
|
|
552
|
-
'into the conversation
|
|
587
|
+
'into the conversation; a uri field is included when the cross-process ' +
|
|
588
|
+
'placement channel is available) and a summary (design name, module count, ' +
|
|
553
589
|
'connection count, module names). Use this when the user wants a whole ' +
|
|
554
590
|
'system/module-level design, not a single schematic or symbol. ' +
|
|
555
591
|
'IMPORTANT: The generated system design renders automatically as a result ' +
|