@huaqiu/dsh-tool-schematic-gen 0.1.1
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 +8 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +106004 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +35 -0
- package/lib/index.mjs +1439 -0
- package/package.json +58 -0
- package/src/client/ecad.ts +147 -0
- package/src/client/hit-card.tsx +378 -0
- package/src/client/i18n.ts +148 -0
- package/src/client/index.ts +176 -0
- package/src/client/login-url.ts +91 -0
- package/src/client/parse.ts +153 -0
- package/src/client/progress.ts +161 -0
- package/src/client/stack-frame.tsx +269 -0
- package/src/client/theme.ts +228 -0
- package/src/client/trace-names.ts +308 -0
- package/src/client/trace-tree.ts +185 -0
- package/src/config.ts +203 -0
- package/src/index.ts +130 -0
- package/src/progress.ts +333 -0
- package/src/routes.ts +80 -0
- package/src/sse.ts +448 -0
- package/src/tools.ts +590 -0
- package/src/trace.ts +336 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/dsh-tool-schematic-gen` — the two agent-visible tools.
|
|
3
|
+
*
|
|
4
|
+
* Faithful TypeScript port of the `hq-edge` plugin's tool bodies, adapted to
|
|
5
|
+
* the published DSH plugin surface:
|
|
6
|
+
* - the eda.cn account always comes from the `huaqiuAuth` service (no demo
|
|
7
|
+
* credentials — migration plan review #9);
|
|
8
|
+
* - generated artifacts are stored in the `huaqiuArtifacts` service
|
|
9
|
+
* (in-process, not a loopback);
|
|
10
|
+
* - tools are `defineTool` with `output.schema = { type: 'json' }` and a
|
|
11
|
+
* structured (lossless-JSON) result.
|
|
12
|
+
*
|
|
13
|
+
* Tools:
|
|
14
|
+
* generate_schematic_from_description description → KiCad schematic
|
|
15
|
+
* generate_system_module_graph description → module graph → KiCad zip
|
|
16
|
+
*
|
|
17
|
+
* @module @huaqiu/dsh-tool-schematic-gen
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from 'node:crypto'
|
|
20
|
+
import { writeFile } from 'node:fs/promises'
|
|
21
|
+
import { tmpdir } from 'node:os'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
24
|
+
import type { HuaqiuAuthService } from '@huaqiu/dsh-auth'
|
|
25
|
+
import type { CreateArtifactResult, HuaqiuArtifacts } from '@huaqiu/dsh-artifacts'
|
|
26
|
+
import {
|
|
27
|
+
agentIds,
|
|
28
|
+
buildHeaders,
|
|
29
|
+
buildRunBody,
|
|
30
|
+
sanitizeZipBaseName,
|
|
31
|
+
type EdaAccount,
|
|
32
|
+
type SchematicGenConfig,
|
|
33
|
+
} from './config.js'
|
|
34
|
+
import { consumeCopilotkit, exportModuleGraphZip, HTTP_TIMEOUT_MS } from './sse.js'
|
|
35
|
+
import type { ProgressNote, RunProgress, TodoItem } from './progress.js'
|
|
36
|
+
import type { TraceEvent } from './trace.js'
|
|
37
|
+
|
|
38
|
+
/** Structural alias of the DSH `JsonValue` (see part-search Phase 1 §15.2.1). */
|
|
39
|
+
type Json = string | number | boolean | null | Json[] | { [key: string]: Json }
|
|
40
|
+
|
|
41
|
+
/** The normalized domain values are lossless-JSON plain objects except that
|
|
42
|
+
* optional fields are `undefined`, which fails DSH's lossless-JSON validation. */
|
|
43
|
+
function asJson<T>(value: T): Json {
|
|
44
|
+
return JSON.parse(JSON.stringify(value)) as Json
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Console tag for log filtering. */
|
|
48
|
+
const LOG_TAG = '[dsh-schematic-gen]'
|
|
49
|
+
|
|
50
|
+
/** Agent-facing timeout hints. */
|
|
51
|
+
export const TOOL_TIMEOUT_MS = {
|
|
52
|
+
generate_schematic_from_description: HTTP_TIMEOUT_MS,
|
|
53
|
+
generate_system_module_graph: HTTP_TIMEOUT_MS,
|
|
54
|
+
} as const
|
|
55
|
+
|
|
56
|
+
/** Zip files up to this size are inlined as a base64 data URL on fallback;
|
|
57
|
+
* larger ones are written to a temp file and returned by path. */
|
|
58
|
+
export const MAX_INLINE_ZIP_BYTES = 1_000_000
|
|
59
|
+
|
|
60
|
+
function renderJson(_args: unknown, value: unknown) {
|
|
61
|
+
return [{ type: 'text' as const, text: JSON.stringify(value) }]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Runtime environment ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export interface SchematicGenDeps {
|
|
67
|
+
fetchImpl?: typeof fetch
|
|
68
|
+
writeFileImpl?: (path: string, data: Buffer | string) => Promise<void>
|
|
69
|
+
tmpDirImpl?: () => string
|
|
70
|
+
uuidImpl?: () => string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SchematicGenEnv {
|
|
74
|
+
config: SchematicGenConfig
|
|
75
|
+
/** `huaqiuAuth` node service — the eda.cn account capability. */
|
|
76
|
+
auth: HuaqiuAuthService['auth']
|
|
77
|
+
/** `huaqiuArtifacts` node service — preview-artifact store. */
|
|
78
|
+
artifacts: HuaqiuArtifacts
|
|
79
|
+
timeoutMs: number
|
|
80
|
+
/**
|
|
81
|
+
* Optional live-progress sink, keyed by the tool call id. Absent in tests
|
|
82
|
+
* and when the `webServer` service is unavailable — progress reporting is
|
|
83
|
+
* best-effort and must never be able to fail a generation.
|
|
84
|
+
*/
|
|
85
|
+
progress?: RunProgress | null
|
|
86
|
+
deps?: SchematicGenDeps
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Resolve the eda.cn account from the auth capability (no baked-in creds). */
|
|
90
|
+
async function resolveAccount(auth: HuaqiuAuthService['auth']): Promise<EdaAccount | null> {
|
|
91
|
+
if (!auth || typeof auth.getUserInfo !== 'function') return null
|
|
92
|
+
try {
|
|
93
|
+
const info = await auth.getUserInfo()
|
|
94
|
+
if (info && typeof info.id === 'string' && typeof info.token === 'string' && info.id.length > 0 && info.token.length > 0) {
|
|
95
|
+
return { userId: info.id, userToken: info.token }
|
|
96
|
+
}
|
|
97
|
+
return null
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.warn(LOG_TAG, 'could not resolve the eda.cn account', String((err as Error)?.message || err))
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Fresh run id, injectable for tests. */
|
|
105
|
+
function newRunId(env: SchematicGenEnv): string {
|
|
106
|
+
return typeof env.deps?.uuidImpl === 'function' ? env.deps.uuidImpl() : randomUUID()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Minimal structural view of DSH's `ToolRunContext`. We only need two fields:
|
|
111
|
+
* `signal` (cancellation) and `callId` — the tool-call identity that the
|
|
112
|
+
* `tool.call.toolview` slot hands to the browser component as `props.callId`.
|
|
113
|
+
* Typed structurally so tests can pass a plain object.
|
|
114
|
+
*/
|
|
115
|
+
export interface ToolExecLike {
|
|
116
|
+
signal?: AbortSignal
|
|
117
|
+
callId?: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface RunProgressHandle {
|
|
121
|
+
onTrace(events: TraceEvent[]): void
|
|
122
|
+
onState(state: Record<string, unknown>): void
|
|
123
|
+
onTodos(todos: TodoItem[]): void
|
|
124
|
+
onNote(note: ProgressNote): void
|
|
125
|
+
done(): void
|
|
126
|
+
failed(message: string): void
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Bind one tool invocation to the progress store.
|
|
131
|
+
*
|
|
132
|
+
* Every returned callback is a no-op when there is no store or no `callId`,
|
|
133
|
+
* so a tool can call these unconditionally.
|
|
134
|
+
*/
|
|
135
|
+
function progressFor(
|
|
136
|
+
env: SchematicGenEnv,
|
|
137
|
+
exec: ToolExecLike | undefined,
|
|
138
|
+
toolName: string,
|
|
139
|
+
kind: 'schematic' | 'system',
|
|
140
|
+
): RunProgressHandle {
|
|
141
|
+
const store = env.progress
|
|
142
|
+
const callId = typeof exec?.callId === 'string' && exec.callId.length > 0 ? exec.callId : ''
|
|
143
|
+
const live = store && callId ? { store, callId } : null
|
|
144
|
+
if (live) live.store.start(live.callId, toolName, kind)
|
|
145
|
+
return {
|
|
146
|
+
onTrace: (events) => { if (live) live.store.pushTrace(live.callId, events) },
|
|
147
|
+
onState: (state) => { if (live) live.store.updateState(live.callId, state) },
|
|
148
|
+
onTodos: (todos) => { if (live) live.store.setTodos(live.callId, todos) },
|
|
149
|
+
onNote: (note) => { if (live) live.store.setNote(live.callId, note) },
|
|
150
|
+
done: () => { if (live) live.store.finish(live.callId) },
|
|
151
|
+
failed: (message) => { if (live) live.store.fail(live.callId, message) },
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Store a generated artifact in the user-wide preview store (in-process). */
|
|
156
|
+
async function createPreviewArtifact(
|
|
157
|
+
env: SchematicGenEnv,
|
|
158
|
+
type: 'schematic' | 'zip',
|
|
159
|
+
filename: string,
|
|
160
|
+
content: string,
|
|
161
|
+
contentEncoding?: 'utf8' | 'base64',
|
|
162
|
+
): Promise<CreateArtifactResult> {
|
|
163
|
+
if (!env.artifacts || typeof env.artifacts.create !== 'function') {
|
|
164
|
+
throw new Error('schematic-gen: huaqiuArtifacts service unavailable — cannot store preview artifact')
|
|
165
|
+
}
|
|
166
|
+
return env.artifacts.create({ type, filename, content, contentEncoding })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Deliverable extraction ───────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export interface SchematicSheet {
|
|
172
|
+
filename: string
|
|
173
|
+
content: string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ExtractedSchematic {
|
|
177
|
+
outProject: string
|
|
178
|
+
project_achieve_url: string
|
|
179
|
+
kicadPro: string
|
|
180
|
+
schFiles: SchematicSheet[]
|
|
181
|
+
error: string
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Pull the schematic deliverable out of the final agent state. `schFiles`
|
|
186
|
+
* carry the `.kicad_sch` content inline, so the text is returned verbatim.
|
|
187
|
+
*/
|
|
188
|
+
export function extractSchematic(state: Record<string, unknown>): ExtractedSchematic {
|
|
189
|
+
const rawFiles = Array.isArray(state.schFiles) ? state.schFiles : []
|
|
190
|
+
const schFiles = rawFiles
|
|
191
|
+
.map((f) => {
|
|
192
|
+
const file = f && typeof f === 'object' ? f as Record<string, unknown> : {}
|
|
193
|
+
return {
|
|
194
|
+
filename: typeof file.filename === 'string' ? file.filename : '',
|
|
195
|
+
content: typeof file.content === 'string'
|
|
196
|
+
? file.content
|
|
197
|
+
: (typeof file.content === 'object' && file.content !== null
|
|
198
|
+
? JSON.stringify(file.content)
|
|
199
|
+
: String(file.content ?? '')),
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
.filter((f) => f.filename.length > 0)
|
|
203
|
+
return {
|
|
204
|
+
outProject: typeof state.outProject === 'string' ? state.outProject : '',
|
|
205
|
+
project_achieve_url: typeof state.project_achieve_url === 'string' ? state.project_achieve_url : '',
|
|
206
|
+
kicadPro: typeof state.kicadPro === 'string' ? state.kicadPro : '',
|
|
207
|
+
schFiles,
|
|
208
|
+
error: typeof state.error === 'string' ? state.error : '',
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Pull the module graph out of the final system-design state. */
|
|
213
|
+
export function extractModuleGraph(state: Record<string, unknown>): Record<string, unknown> | null {
|
|
214
|
+
const mg = state && state.module_graph
|
|
215
|
+
return mg && typeof mg === 'object' ? (mg as Record<string, unknown>) : null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Shared tail: materialize schematic artifacts ─────────────────────────────
|
|
219
|
+
|
|
220
|
+
interface MaterializedSchematic {
|
|
221
|
+
schFiles: Array<{ filename: string; content?: string }>
|
|
222
|
+
schArtifacts?: Array<{ id: string; type: string; filename: string; size: number }>
|
|
223
|
+
/** User-safe status detail — the client card renders this. */
|
|
224
|
+
note?: string
|
|
225
|
+
/** Agent-only explanation/directive — the client card MUST NOT render it. */
|
|
226
|
+
agentNote?: string
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Store each `.kicad_sch` sheet as a preview artifact and return the
|
|
231
|
+
* structured result. Artifact creation is BEST-EFFORT and non-fatal: on any
|
|
232
|
+
* failure the raw content is preserved inline so the result card can still
|
|
233
|
+
* render it.
|
|
234
|
+
*/
|
|
235
|
+
async function materializeSchematicArtifacts(env: SchematicGenEnv, schFiles: SchematicSheet[]): Promise<MaterializedSchematic> {
|
|
236
|
+
const outFiles: Array<{ filename: string; content?: string }> = schFiles.map((f) => ({ filename: f.filename }))
|
|
237
|
+
const artifacts: Array<{ id: string; type: string; filename: string; size: number }> = []
|
|
238
|
+
let anyFailed = false
|
|
239
|
+
let errorNote = ''
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < schFiles.length; i++) {
|
|
242
|
+
const file = schFiles[i]!
|
|
243
|
+
try {
|
|
244
|
+
const created = await createPreviewArtifact(env, 'schematic', file.filename, file.content)
|
|
245
|
+
artifacts.push({ id: created.id, type: created.type, filename: created.filename, size: created.size })
|
|
246
|
+
} catch (storeErr) {
|
|
247
|
+
anyFailed = true
|
|
248
|
+
outFiles[i]!.content = file.content // data-loss guard
|
|
249
|
+
const msg = String((storeErr as Error)?.message || storeErr)
|
|
250
|
+
errorNote += (errorNote ? '; ' : '') + file.filename + ': ' + msg
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const result: MaterializedSchematic = { schFiles: outFiles }
|
|
255
|
+
if (artifacts.length > 0) result.schArtifacts = artifacts
|
|
256
|
+
if (anyFailed) {
|
|
257
|
+
// Split by audience: the degraded state is worth telling the human, but
|
|
258
|
+
// "the result card can render them directly / the full source is in the
|
|
259
|
+
// zip" is an explanation for the agent about why it need not worry.
|
|
260
|
+
result.note = 'Preview artifact storage partially or fully unavailable (' + errorNote + ').'
|
|
261
|
+
result.agentNote =
|
|
262
|
+
'Sheets without an artifact id still carry their source inline, so the result card can ' +
|
|
263
|
+
'render them directly. Full source is always in the project zip/export.'
|
|
264
|
+
}
|
|
265
|
+
return result
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Tool bodies ──────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Structured `needs_auth` result returned when the eda.cn login is missing —
|
|
272
|
+
* the signal that makes login a human-in-the-loop step. The web client
|
|
273
|
+
* (dsh-auth client half) renders a login card with an embedded auth.eda.cn
|
|
274
|
+
* iframe for this result; the model asks the user to complete the login and
|
|
275
|
+
* then retries the tool. Throwing here would hide that HIT surface.
|
|
276
|
+
*/
|
|
277
|
+
export function needsAuth(kind: 'schematic' | 'system'): Record<string, unknown> {
|
|
278
|
+
return {
|
|
279
|
+
status: 'needs_auth',
|
|
280
|
+
kind,
|
|
281
|
+
hint:
|
|
282
|
+
'This tool requires a Huaqiu EDA (eda.cn) login. The web client is showing ' +
|
|
283
|
+
'a login card with an embedded eda.cn login iframe — ask the user to complete ' +
|
|
284
|
+
'the login there (or use the 华秋EDA login button in the sidebar), then call ' +
|
|
285
|
+
'this tool again.',
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* `generate_schematic_from_description` body — stream `schemagen`, extract the
|
|
291
|
+
* inline `.kicad_sch` files, then store each sheet as a preview artifact.
|
|
292
|
+
*/
|
|
293
|
+
export async function runGenerateSchematic(
|
|
294
|
+
args: Record<string, unknown>,
|
|
295
|
+
exec: ToolExecLike | undefined,
|
|
296
|
+
env: SchematicGenEnv,
|
|
297
|
+
): Promise<Record<string, unknown>> {
|
|
298
|
+
const account = await resolveAccount(env.auth)
|
|
299
|
+
if (!account) return needsAuth('schematic')
|
|
300
|
+
const threadId = newRunId(env)
|
|
301
|
+
const body = buildRunBody(
|
|
302
|
+
agentIds.SCHEMATIC,
|
|
303
|
+
typeof args.description === 'string' ? args.description : '',
|
|
304
|
+
env.config,
|
|
305
|
+
account,
|
|
306
|
+
typeof args.user_language === 'string' ? args.user_language : undefined,
|
|
307
|
+
threadId,
|
|
308
|
+
)
|
|
309
|
+
const prog = progressFor(env, exec, 'generate_schematic_from_description', 'schematic')
|
|
310
|
+
let state: Record<string, unknown>
|
|
311
|
+
let text: string
|
|
312
|
+
try {
|
|
313
|
+
const res = await consumeCopilotkit(env.config.copilotkitUrl, body, buildHeaders(env.config, account, threadId), {
|
|
314
|
+
signal: exec?.signal,
|
|
315
|
+
timeoutMs: env.timeoutMs,
|
|
316
|
+
fetchImpl: env.deps?.fetchImpl,
|
|
317
|
+
onTrace: prog.onTrace,
|
|
318
|
+
onState: prog.onState,
|
|
319
|
+
onUnauthorized: () => { void env.auth.logout() },
|
|
320
|
+
})
|
|
321
|
+
state = res.state
|
|
322
|
+
text = res.text
|
|
323
|
+
} catch (err) {
|
|
324
|
+
prog.failed(String((err as Error)?.message || err))
|
|
325
|
+
throw err
|
|
326
|
+
}
|
|
327
|
+
const extracted = extractSchematic(state)
|
|
328
|
+
if (extracted.error) {
|
|
329
|
+
const message = 'schematic-gen: the schematic agent finished with an error: ' + extracted.error +
|
|
330
|
+
(text ? ' — ' + text.slice(0, 300) : '')
|
|
331
|
+
prog.failed(message)
|
|
332
|
+
throw new Error(message)
|
|
333
|
+
}
|
|
334
|
+
if (extracted.schFiles.length === 0) {
|
|
335
|
+
const message = 'schematic-gen: the schematic agent produced no .kicad_sch files.' +
|
|
336
|
+
(text ? ' Assistant said: ' + text.slice(0, 300) : '')
|
|
337
|
+
prog.failed(message)
|
|
338
|
+
throw new Error(message)
|
|
339
|
+
}
|
|
340
|
+
const materialized = await materializeSchematicArtifacts(env, extracted.schFiles)
|
|
341
|
+
const result: Record<string, unknown> = {
|
|
342
|
+
status: 'generated',
|
|
343
|
+
kind: 'schematic',
|
|
344
|
+
design_name: extracted.outProject || '',
|
|
345
|
+
schFiles: materialized.schFiles,
|
|
346
|
+
schArtifacts: materialized.schArtifacts,
|
|
347
|
+
kicadPro: extracted.kicadPro,
|
|
348
|
+
project_achieve_url: extracted.project_achieve_url,
|
|
349
|
+
}
|
|
350
|
+
if (materialized.note) result.note = materialized.note
|
|
351
|
+
if (materialized.agentNote) result.agentNote = materialized.agentNote
|
|
352
|
+
prog.done()
|
|
353
|
+
return result
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* `generate_system_module_graph` body — stream `modular_circuit`, extract the
|
|
358
|
+
* module graph, POST it to export-zip, return the KiCad project zip stored as
|
|
359
|
+
* a `zip` preview artifact.
|
|
360
|
+
*/
|
|
361
|
+
export async function runGenerateSystem(
|
|
362
|
+
args: Record<string, unknown>,
|
|
363
|
+
exec: ToolExecLike | undefined,
|
|
364
|
+
env: SchematicGenEnv,
|
|
365
|
+
): Promise<Record<string, unknown>> {
|
|
366
|
+
const account = await resolveAccount(env.auth)
|
|
367
|
+
if (!account) return needsAuth('system')
|
|
368
|
+
const threadId = newRunId(env)
|
|
369
|
+
const body = buildRunBody(
|
|
370
|
+
agentIds.SYSTEM,
|
|
371
|
+
typeof args.description === 'string' ? args.description : '',
|
|
372
|
+
env.config,
|
|
373
|
+
account,
|
|
374
|
+
typeof args.user_language === 'string' ? args.user_language : undefined,
|
|
375
|
+
threadId,
|
|
376
|
+
)
|
|
377
|
+
const prog = progressFor(env, exec, 'generate_system_module_graph', 'system')
|
|
378
|
+
let state: Record<string, unknown>
|
|
379
|
+
let text: string
|
|
380
|
+
try {
|
|
381
|
+
const res = await consumeCopilotkit(env.config.copilotkitUrl, body, buildHeaders(env.config, account, threadId), {
|
|
382
|
+
signal: exec?.signal,
|
|
383
|
+
timeoutMs: env.timeoutMs,
|
|
384
|
+
fetchImpl: env.deps?.fetchImpl,
|
|
385
|
+
onTrace: prog.onTrace,
|
|
386
|
+
onState: prog.onState,
|
|
387
|
+
onTodos: prog.onTodos,
|
|
388
|
+
onNote: prog.onNote,
|
|
389
|
+
// `modular_circuit` does NOT emit CUSTOM trace events — its stack
|
|
390
|
+
// arrives as the standard AG-UI tool-call lifecycle. Without this the
|
|
391
|
+
// system-design card reported no progress at all.
|
|
392
|
+
toolCallTrace: true,
|
|
393
|
+
onUnauthorized: () => { void env.auth.logout() },
|
|
394
|
+
})
|
|
395
|
+
state = res.state
|
|
396
|
+
text = res.text
|
|
397
|
+
} catch (err) {
|
|
398
|
+
prog.failed(String((err as Error)?.message || err))
|
|
399
|
+
throw err
|
|
400
|
+
}
|
|
401
|
+
const moduleGraph = extractModuleGraph(state)
|
|
402
|
+
if (!moduleGraph) {
|
|
403
|
+
const errState = typeof state.error === 'string' && state.error ? state.error : ''
|
|
404
|
+
const message = 'schematic-gen: the system design agent produced no module_graph.' +
|
|
405
|
+
(errState ? ' Error: ' + errState : '') +
|
|
406
|
+
(text ? ' Assistant said: ' + text.slice(0, 300) : '')
|
|
407
|
+
prog.failed(message)
|
|
408
|
+
throw new Error(message)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// The stage ladder already reports "export" (module_graph is filled) while
|
|
412
|
+
// this POST runs, so no extra phase marker is needed here.
|
|
413
|
+
const zipBuf = await exportModuleGraphZip(env.config.exportZipUrl, moduleGraph, env.config, account, {
|
|
414
|
+
signal: exec?.signal,
|
|
415
|
+
timeoutMs: env.timeoutMs,
|
|
416
|
+
fetchImpl: env.deps?.fetchImpl,
|
|
417
|
+
}).catch((err: unknown) => {
|
|
418
|
+
prog.failed(String((err as Error)?.message || err))
|
|
419
|
+
throw err
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const designName = typeof state.design_name === 'string' && state.design_name
|
|
423
|
+
? state.design_name
|
|
424
|
+
: 'circuit'
|
|
425
|
+
const connectionCount = typeof state.connection_count === 'number'
|
|
426
|
+
? state.connection_count
|
|
427
|
+
: (Array.isArray(moduleGraph.connections) ? moduleGraph.connections.length : 0)
|
|
428
|
+
const moduleNames = Array.isArray(moduleGraph.modules)
|
|
429
|
+
? (moduleGraph.modules as Array<Record<string, unknown>>)
|
|
430
|
+
.map((m) => (m && typeof m.name === 'string' ? m.name : ''))
|
|
431
|
+
.filter((n) => n.length > 0)
|
|
432
|
+
: []
|
|
433
|
+
|
|
434
|
+
const result: Record<string, unknown> = {
|
|
435
|
+
status: 'generated',
|
|
436
|
+
kind: 'system',
|
|
437
|
+
design_name: designName,
|
|
438
|
+
module_count: moduleNames.length,
|
|
439
|
+
connection_count: connectionCount,
|
|
440
|
+
module_names: moduleNames,
|
|
441
|
+
zip_bytes: zipBuf.length,
|
|
442
|
+
}
|
|
443
|
+
const notes: string[] = []
|
|
444
|
+
|
|
445
|
+
// Store the project zip as a `zip` preview artifact (primary). Keeping the
|
|
446
|
+
// zip OUT of the JSON keeps the result small — inlining base64 used to
|
|
447
|
+
// truncate the tool result and fail the card.
|
|
448
|
+
let zipArtifact: { id: string; type: string; filename: string; size: number } | null = null
|
|
449
|
+
try {
|
|
450
|
+
const safeName = sanitizeZipBaseName(designName)
|
|
451
|
+
const created = await createPreviewArtifact(env, 'zip', safeName + '.zip', zipBuf.toString('base64'), 'base64')
|
|
452
|
+
zipArtifact = { id: created.id, type: created.type, filename: created.filename, size: created.size }
|
|
453
|
+
} catch (storeErr) {
|
|
454
|
+
notes.push('Could not store the project zip as an artifact (' +
|
|
455
|
+
String((storeErr as Error)?.message || storeErr) + '); kept it in the result instead.')
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (zipArtifact) {
|
|
459
|
+
result.zipArtifact = zipArtifact
|
|
460
|
+
} else {
|
|
461
|
+
// Fallback (artifact store unavailable): inline the zip when small,
|
|
462
|
+
// otherwise write to a temp file and return the path.
|
|
463
|
+
if (zipBuf.length <= MAX_INLINE_ZIP_BYTES) {
|
|
464
|
+
result.zip = 'data:application/zip;base64,' + zipBuf.toString('base64')
|
|
465
|
+
} else {
|
|
466
|
+
const safeName = sanitizeZipBaseName(designName)
|
|
467
|
+
const fileName = 'hq-eda-' + safeName + '-' + newRunId(env).slice(0, 8) + '.zip'
|
|
468
|
+
const dir = (env.deps?.tmpDirImpl && env.deps.tmpDirImpl()) || tmpdir()
|
|
469
|
+
const filePath = join(dir, fileName)
|
|
470
|
+
try {
|
|
471
|
+
await (env.deps?.writeFileImpl || writeFile)(filePath, zipBuf)
|
|
472
|
+
result.zip_path = filePath
|
|
473
|
+
} catch (err) {
|
|
474
|
+
result.zip = 'data:application/zip;base64,' + zipBuf.toString('base64')
|
|
475
|
+
notes.push('Could not write the zip to a temp file (' +
|
|
476
|
+
String((err as Error)?.message || err) + '); returned inline instead.')
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (notes.length > 0) result.note = notes.join(' ')
|
|
481
|
+
prog.done()
|
|
482
|
+
return result
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Agent-awareness note about the eda.cn login gate, appended to both tool
|
|
486
|
+
* descriptions: a `needs_auth` result surfaces a login HIT (embedded eda.cn
|
|
487
|
+
* iframe card) — wait for the user to log in, then retry. Never invent
|
|
488
|
+
* credentials or fake success. */
|
|
489
|
+
const AUTH_GATE_NOTE =
|
|
490
|
+
'AUTH: This tool requires a Huaqiu EDA (eda.cn) account. If the result has ' +
|
|
491
|
+
'status "needs_auth", the web client is showing a login card with an embedded ' +
|
|
492
|
+
'eda.cn login iframe (the human-in-the-loop step). Ask the user to complete the ' +
|
|
493
|
+
'login there or via the 华秋EDA login button in the sidebar (you may use ' +
|
|
494
|
+
'ask_user_question to wait, offering a "retry now that I have logged in" ' +
|
|
495
|
+
'option and a "cancel" option — phrase BOTH in the language the user is ' +
|
|
496
|
+
'writing in), then call this tool again. Never invent credentials and never ' +
|
|
497
|
+
'claim success when the result is needs_auth.'
|
|
498
|
+
|
|
499
|
+
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
function createSchematicTool(env: SchematicGenEnv) {
|
|
502
|
+
return defineTool({
|
|
503
|
+
name: 'generate_schematic_from_description',
|
|
504
|
+
description:
|
|
505
|
+
'Generate a KiCad schematic (.kicad_sch files) from a natural-language ' +
|
|
506
|
+
'description of a circuit or sub-circuit — e.g. "design a 5V LM7805 linear ' +
|
|
507
|
+
'regulator power supply with input and output filter capacitors". Calls the ' +
|
|
508
|
+
'online HQ-EDA schematic generation agent and returns ' +
|
|
509
|
+
'schFiles (filename references), schArtifacts (preview artifact references ' +
|
|
510
|
+
'with id/type/filename/size per sheet), kicadPro and project_achieve_url. ' +
|
|
511
|
+
'Use this when the user asks to draw, generate or create a circuit ' +
|
|
512
|
+
'schematic from a description (not from an image — for that use the ' +
|
|
513
|
+
'symbol/footprint tools). ' +
|
|
514
|
+
'IMPORTANT: The generated schematic renders automatically as a result card ' +
|
|
515
|
+
'in the web client — an interactive canvas preview per sheet (multi-sheet ' +
|
|
516
|
+
'results get a sheet tab bar) and a download button for the current sheet. ' +
|
|
517
|
+
'Do NOT paste the schematic source, file URLs, or any fenced code block ' +
|
|
518
|
+
'into your reply; just note in one line that the schematic was generated ' +
|
|
519
|
+
'and how many sheets it has. ' + AUTH_GATE_NOTE,
|
|
520
|
+
parameters: {
|
|
521
|
+
description: {
|
|
522
|
+
type: 'string',
|
|
523
|
+
required: true,
|
|
524
|
+
description: 'The circuit design prompt, in natural language. Be specific about components, voltages and any required behaviour.',
|
|
525
|
+
},
|
|
526
|
+
user_language: {
|
|
527
|
+
type: 'string',
|
|
528
|
+
// The fallback is deliberately NOT spelled out here: it is deployment
|
|
529
|
+
// config (`HQ_EDA_DEFAULT_LANGUAGE`), so quoting a literal default in
|
|
530
|
+
// agent-facing copy would go stale the moment it is overridden.
|
|
531
|
+
description: 'Optional language hint for the agent — pass the language the user is writing in (e.g. "简体中文" or "English"). Omit to use the deployment default.',
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
535
|
+
async execute(args, exec) {
|
|
536
|
+
return asJson(await runGenerateSchematic(args, exec, env))
|
|
537
|
+
},
|
|
538
|
+
timeoutMs: TOOL_TIMEOUT_MS.generate_schematic_from_description,
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function createSystemTool(env: SchematicGenEnv) {
|
|
543
|
+
return defineTool({
|
|
544
|
+
name: 'generate_system_module_graph',
|
|
545
|
+
description:
|
|
546
|
+
'Generate a hardware system design (module graph) from a natural-language ' +
|
|
547
|
+
'description — e.g. "design a small smart alarm clock". Calls the online ' +
|
|
548
|
+
'HQ-EDA system-design agent, which plans the modules, searches/selects ' +
|
|
549
|
+
'parts, wires the connections, and produces a module graph; the graph is ' +
|
|
550
|
+
'then exported to a KiCad project zip. Returns: a zipArtifact reference ' +
|
|
551
|
+
'(preview-artifact id of the full project zip — the zip is never inlined ' +
|
|
552
|
+
'into the conversation) and a summary (design name, module count, ' +
|
|
553
|
+
'connection count, module names). Use this when the user wants a whole ' +
|
|
554
|
+
'system/module-level design, not a single schematic or symbol. ' +
|
|
555
|
+
'IMPORTANT: The generated system design renders automatically as a result ' +
|
|
556
|
+
'card in the web client — a canvas preview of the project root schematic ' +
|
|
557
|
+
'(fetched from the zip artifact) and a Download button for the full ' +
|
|
558
|
+
'project zip. Do NOT paste the schematic source, file URLs, or any fenced ' +
|
|
559
|
+
'code block into your reply; just note in one line that the design was ' +
|
|
560
|
+
'generated, its module count, and that the project zip is downloadable ' +
|
|
561
|
+
'from the card. ' + AUTH_GATE_NOTE,
|
|
562
|
+
parameters: {
|
|
563
|
+
description: {
|
|
564
|
+
type: 'string',
|
|
565
|
+
required: true,
|
|
566
|
+
description: 'The system design prompt, in natural language. Describe the product or function you want, e.g. "an ESP32-C3 based smart fan".',
|
|
567
|
+
},
|
|
568
|
+
user_language: {
|
|
569
|
+
type: 'string',
|
|
570
|
+
// The fallback is deliberately NOT spelled out here: it is deployment
|
|
571
|
+
// config (`HQ_EDA_DEFAULT_LANGUAGE`), so quoting a literal default in
|
|
572
|
+
// agent-facing copy would go stale the moment it is overridden.
|
|
573
|
+
description: 'Optional language hint for the agent — pass the language the user is writing in (e.g. "简体中文" or "English"). Omit to use the deployment default.',
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
577
|
+
async execute(args, exec) {
|
|
578
|
+
return asJson(await runGenerateSystem(args, exec, env))
|
|
579
|
+
},
|
|
580
|
+
timeoutMs: TOOL_TIMEOUT_MS.generate_system_module_graph,
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Build the two tool definitions against a runtime env. */
|
|
585
|
+
export function createSchematicGenTools(env: SchematicGenEnv) {
|
|
586
|
+
return [
|
|
587
|
+
createSchematicTool(env),
|
|
588
|
+
createSystemTool(env),
|
|
589
|
+
]
|
|
590
|
+
}
|