@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.
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Display names for the agents' graph nodes and tool calls (zh + en).
3
+ *
4
+ * The eda.cn backend streams raw code identifiers — `node:schematicDesign`,
5
+ * `search_parts`, `es_rag_search` — and the call stack used to render them
6
+ * verbatim, so a Chinese UI showed an English snake_case stack. This module is
7
+ * the dictionary that turns an identifier into a sentence.
8
+ *
9
+ * Port of `hq-eda-ai`'s two resolvers:
10
+ *
11
+ * - schematic → `apps/web/src/components/sch_sub_gen/TraceTimeline.tsx`
12
+ * (`resolveName`: `SchematicGen.Trace.Nodes` → `.Tools` → `ToolCalls`)
13
+ * - system → `apps/web/src/components/modular_circuit/ModuleGenTraceTimeline.tsx`
14
+ * (`resolveName`: `ToolCalls` only)
15
+ *
16
+ * with the dictionaries taken from `apps/web/src/locales/{cn,en}.ts` — the
17
+ * `ToolCalls` table plus the `SchematicGen.Trace.{Nodes,Tools}` overrides.
18
+ *
19
+ * Names that are NOT in any dictionary are returned unchanged on purpose:
20
+ * the module worker instances are named after the module the agent is working
21
+ * on ("Type-C USB2.0 接口保护模块"), and those are already copy, not code.
22
+ */
23
+ import { useCallback } from 'react'
24
+ import type { AuthLocale } from './login-url.js'
25
+ import { useLocale } from './theme.js'
26
+
27
+ /** Prefix `traceName()` puts on a LangGraph node so it cannot collide with a tool. */
28
+ const NODE_PREFIX = 'node:'
29
+
30
+ // ── Shared node / tool vocabulary ──────────────────────────────────────────
31
+ // The system-design agent (`modular_circuit`) resolves EVERYTHING through this
32
+ // one table. The schematic agent uses it as its last resort — which is where
33
+ // `es_rag_search` / `es_category_search` / `es_precise_search` come from, since
34
+ // they are absent from `SchematicGen.Trace.Tools`.
35
+
36
+ /**
37
+ * Chinese is the source of truth, as in `client/i18n.ts`: `CallNameKey` is
38
+ * derived from it and `TOOL_CALLS_EN` is typed against it, so a name added to
39
+ * zh but missing from en is a COMPILE error rather than a Chinese fallback
40
+ * leaking into an English UI.
41
+ */
42
+ const TOOL_CALLS_ZH = {
43
+ // Search
44
+ plan_search: '搜索规划',
45
+ es_category_search: '分类搜索',
46
+ es_precise_search: '精准搜索',
47
+ es_rag_search: '语义搜索',
48
+ ComponentSearchOutput: '元件检索',
49
+ // Routing / agent
50
+ intent_resolver: '路由决策',
51
+ generate_design_plan: '生成设计方案',
52
+ reflect_design: '设计反思',
53
+ module_ops_agent: '模块管理助手',
54
+ // TypeScript graph nodes
55
+ intentResolver: '需求分析',
56
+ generateDesignPlan: '生成设计方案',
57
+ moduleOpsAgent: '模块管理',
58
+ designAgent: '系统设计',
59
+ model_request: '分析与决策',
60
+ tools: '执行工具',
61
+ design_plan_gen: '生成设计方案',
62
+ module_search: '模块搜索',
63
+ module_connect: '模块连接',
64
+ request_module_change: '模块替换',
65
+ prepareSearch: '准备搜索任务',
66
+ planSearch: '搜索规划',
67
+ componentWorker: '器件搜索',
68
+ syncModules: '同步模块',
69
+ connectModules: '模块连接设计',
70
+ buildModuleGraph: '生成连接图',
71
+ request_design_decision: '确认设计决策',
72
+ request_module_review: '确认模块变更',
73
+ reflectDesign: '设计复核',
74
+ ercCheckNode: '电气规则检查',
75
+ exportCircuit: '导出电路',
76
+ schematicDesign: '原理图设计',
77
+ checkCircuit: '电路校验',
78
+ datasheetReview: '数据手册评审',
79
+ layoutAnnotate: '布局规划',
80
+ integrateFlat: '生成原理图',
81
+ genPreviewUrl: '生成预览',
82
+ // Component / module operations
83
+ component_worker: '搜索元器件',
84
+ search_modules: '搜索模块',
85
+ add_modules: '添加模块',
86
+ replace_modules: '替换模块',
87
+ rm_modules: '移除模块',
88
+ complete_modules: '补全模块',
89
+ connect_modules: '连接模块',
90
+ // Design / schematic
91
+ kicad_schematic_generate: '生成 KiCad 原理图',
92
+ generate_design_outline: '生成流程图',
93
+ write_connection: '写入连接',
94
+ update_virtual_module_ports: '更新模块端口',
95
+ submit_connection_report: '提交设计报告',
96
+ // Engineering / validation
97
+ erc_check: '电气规则检查',
98
+ add_new_eco: '添加 ECO',
99
+ apply_user_option: '应用用户选项',
100
+ // BOM
101
+ update_bom: '更新 BOM',
102
+ add_module: '添加模块',
103
+ delete_module: '删除模块',
104
+ research_module: '重新搜索模块',
105
+ replace_module: '替换模块',
106
+ update_quantity: '更新数量',
107
+ } as const
108
+
109
+ type CallNameKey = keyof typeof TOOL_CALLS_ZH
110
+
111
+ const TOOL_CALLS_EN: Record<CallNameKey, string> = {
112
+ plan_search: 'Search Planning',
113
+ es_category_search: 'Category Search',
114
+ es_precise_search: 'Precise Search',
115
+ es_rag_search: 'Semantic Search',
116
+ ComponentSearchOutput: 'Component Retrieval',
117
+ intent_resolver: 'Routing Decision',
118
+ generate_design_plan: 'Generate Design Plan',
119
+ reflect_design: 'Design Reflection',
120
+ module_ops_agent: 'Module Operations Agent',
121
+ intentResolver: 'Requirements Analysis',
122
+ generateDesignPlan: 'Design Plan Generation',
123
+ moduleOpsAgent: 'Module Operations',
124
+ designAgent: 'System Design',
125
+ model_request: 'Analysis and Decision',
126
+ tools: 'Run Tools',
127
+ design_plan_gen: 'Generate Design Plan',
128
+ module_search: 'Module Search',
129
+ module_connect: 'Module Connection',
130
+ request_module_change: 'Module Replacement',
131
+ prepareSearch: 'Prepare Search Tasks',
132
+ planSearch: 'Component Search Planning',
133
+ componentWorker: 'Component Search',
134
+ syncModules: 'Module Synchronization',
135
+ connectModules: 'Module Interconnection Design',
136
+ buildModuleGraph: 'Build Connection Graph',
137
+ request_design_decision: 'Confirm Design Decision',
138
+ request_module_review: 'Review Module Changes',
139
+ reflectDesign: 'Design Review',
140
+ ercCheckNode: 'Electrical Rules Validation',
141
+ exportCircuit: 'Circuit Export',
142
+ schematicDesign: 'Schematic Design',
143
+ checkCircuit: 'Circuit Validation',
144
+ datasheetReview: 'Datasheet Review',
145
+ layoutAnnotate: 'Schematic Layout Planning',
146
+ integrateFlat: 'Schematic Generation',
147
+ genPreviewUrl: 'Preview Rendering',
148
+ component_worker: 'Search Components',
149
+ search_modules: 'Search Modules',
150
+ add_modules: 'Add Modules',
151
+ replace_modules: 'Replace Modules',
152
+ rm_modules: 'Remove Modules',
153
+ complete_modules: 'Complete Modules',
154
+ connect_modules: 'Connect Modules',
155
+ kicad_schematic_generate: 'Generate KiCad Schematic',
156
+ generate_design_outline: 'Generate Diagram',
157
+ write_connection: 'Write Connections',
158
+ update_virtual_module_ports: 'Update Module Ports',
159
+ submit_connection_report: 'Submit Design Report',
160
+ erc_check: 'Electrical Rules Check',
161
+ add_new_eco: 'Add ECO',
162
+ apply_user_option: 'Apply User Options',
163
+ update_bom: 'Update BOM',
164
+ add_module: 'Add Module',
165
+ delete_module: 'Delete Module',
166
+ research_module: 'Research Module',
167
+ replace_module: 'Replace Module',
168
+ update_quantity: 'Update Quantity',
169
+ }
170
+
171
+ // ── Schematic-gen overrides ────────────────────────────────────────────────
172
+ // The schematic agent reuses some identifiers with a different, more specific
173
+ // meaning than `TOOL_CALLS` gives them — `schematicDesign` is "原理图设计"
174
+ // generically but "需求解析与电路设计" as a schematic node, and `search_parts`
175
+ // is the subagent (子代理) when it is a node but plain 物料搜索与选型 when it is
176
+ // a tool. These two tables are consulted BEFORE `TOOL_CALLS`.
177
+
178
+ const SCH_NODES_ZH = {
179
+ schematicDesign: '需求解析与电路设计',
180
+ datasheetReview: '数据手册设计复核',
181
+ checkCircuit: '电气连接完整性检查',
182
+ layoutAnnotate: '原理图布局规划',
183
+ integrateFlat: '生成 KiCad 原理图',
184
+ genPreviewUrl: '生成原理图预览',
185
+ search_parts: '物料搜索与选型子代理',
186
+ circuit_review: '电路设计评审子代理',
187
+ } as const
188
+
189
+ type SchNodeKey = keyof typeof SCH_NODES_ZH
190
+
191
+ const SCH_NODES_EN: Record<SchNodeKey, string> = {
192
+ schematicDesign: 'Circuit Design Engineering',
193
+ datasheetReview: 'Design Review',
194
+ checkCircuit: 'Electrical Design Validation',
195
+ layoutAnnotate: 'Schematic Layout Planning',
196
+ integrateFlat: 'Schematic Generation',
197
+ genPreviewUrl: 'Preview Rendering',
198
+ search_parts: 'Component Selection Subagent',
199
+ circuit_review: 'Circuit Design Review Subagent',
200
+ }
201
+
202
+ const SCH_TOOLS_ZH = {
203
+ symbol_search: '器件符号检索',
204
+ ic_search: '芯片型号检索',
205
+ search_parts: '物料搜索与选型',
206
+ research_datasheet: '数据手册研究',
207
+ submit_module: '提交并校验电路设计',
208
+ check_ic: '芯片应用电路检查',
209
+ ic_datasheet_search: '芯片数据手册检索',
210
+ circuit_review: '电路设计评审',
211
+ } as const
212
+
213
+ type SchToolKey = keyof typeof SCH_TOOLS_ZH
214
+
215
+ const SCH_TOOLS_EN: Record<SchToolKey, string> = {
216
+ symbol_search: 'Component Search',
217
+ ic_search: 'IC Search',
218
+ search_parts: 'Component Selection',
219
+ research_datasheet: 'Datasheet Research',
220
+ submit_module: 'Design Submission',
221
+ check_ic: 'IC Application Check',
222
+ ic_datasheet_search: 'IC Datasheet Search',
223
+ circuit_review: 'Circuit Design Review',
224
+ }
225
+
226
+ // ── Packs ──────────────────────────────────────────────────────────────────
227
+
228
+ interface NamePack {
229
+ /** Schematic graph nodes (checked first, schematic runs only). */
230
+ nodes: Record<string, string>
231
+ /** Schematic tool names (checked second). */
232
+ tools: Record<string, string>
233
+ /** Shared node + tool vocabulary (system runs use only this one). */
234
+ calls: Record<string, string>
235
+ }
236
+
237
+ const PACKS: Record<AuthLocale, NamePack> = {
238
+ zh: {
239
+ nodes: SCH_NODES_ZH as Record<string, string>,
240
+ tools: SCH_TOOLS_ZH as Record<string, string>,
241
+ calls: TOOL_CALLS_ZH as Record<string, string>,
242
+ },
243
+ en: {
244
+ nodes: SCH_NODES_EN as Record<string, string>,
245
+ tools: SCH_TOOLS_EN as Record<string, string>,
246
+ calls: TOOL_CALLS_EN as Record<string, string>,
247
+ },
248
+ }
249
+
250
+ /** Exported for tests: the raw packs, so zh/en parity can be asserted. */
251
+ export const TRACE_NAME_PACKS = PACKS
252
+
253
+ /** Which agent produced the run — decides which tables apply. */
254
+ export type TraceKind = 'schematic' | 'system'
255
+
256
+ /**
257
+ * Turn one raw trace identifier into the localized display name.
258
+ *
259
+ * Resolution order for a schematic run mirrors `TraceTimeline#resolveName`:
260
+ * `node:<name>` → schematic nodes → schematic tools → shared table → the bare
261
+ * name. The last step is what keeps module instance titles intact.
262
+ */
263
+ export function resolveTraceName(raw: string, kind: TraceKind, locale: AuthLocale): string {
264
+ if (typeof raw !== 'string' || raw.length === 0) {
265
+ return raw
266
+ }
267
+ const pack = PACKS[locale] ?? PACKS.zh
268
+ const name = raw.startsWith(NODE_PREFIX) ? raw.slice(NODE_PREFIX.length) : raw
269
+
270
+ // System design has no per-agent override tables: one flat lookup, and
271
+ // anything unknown (a module worker instance named after its module) passes
272
+ // straight through.
273
+ if (kind === 'system') {
274
+ return pack.calls[name] ?? name
275
+ }
276
+
277
+ if (pack.nodes[name]) {
278
+ return pack.nodes[name]!
279
+ }
280
+ const asTool = pack.tools[name] ?? pack.calls[name]
281
+ if (asTool) {
282
+ return asTool
283
+ }
284
+
285
+ // `base:suffix` forms render as "Label · suffix". Unlike hq-eda-ai — which
286
+ // drops the suffix whenever the base is unknown (`tools[base] ?? base`) —
287
+ // we only split when the base IS known, so a name that merely contains a
288
+ // colon survives intact.
289
+ const sep = name.indexOf(':')
290
+ if (sep > 0 && sep < name.length - 1) {
291
+ const base = name.slice(0, sep)
292
+ const label = pack.tools[base] ?? pack.calls[base]
293
+ if (label) {
294
+ return `${label} · ${name.slice(sep + 1)}`
295
+ }
296
+ }
297
+
298
+ return name
299
+ }
300
+
301
+ /** `resolveTraceName` bound to the host UI language. */
302
+ export function useTraceNames(kind: TraceKind): (name: string) => string {
303
+ const locale = useLocale()
304
+ return useCallback(
305
+ (name: string) => resolveTraceName(name, kind, locale),
306
+ [kind, locale],
307
+ )
308
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Flat trace frames → nested call stack (browser half).
3
+ *
4
+ * Port of `hq-eda-ai`'s prototype
5
+ * (`docs/prototype/tool_call_stack_prototype.tsx#buildTree`), adapted to the
6
+ * paired {@link TraceFrame} list produced on the node side rather than to raw
7
+ * events.
8
+ *
9
+ * Each frame carries a `parent>child>leaf` breadcrumb in `path`. This module
10
+ * turns those breadcrumbs into a tree, synthesizing intermediate frames for
11
+ * path segments that never emitted an event of their own — exactly the
12
+ * "Recursive Depth Model" the prototype describes.
13
+ */
14
+ import { stripTaskIds, type TraceFrame, type TraceStatus } from '../trace.js'
15
+
16
+ /**
17
+ * One node of the rendered call stack.
18
+ *
19
+ * A row is collapsed — i.e. represents several invocations of the same
20
+ * logical call at the same visible scope — whenever `repeat > 1`. The runtime
21
+ * keeps the LATEST frame on `frame`, so the duration column shows the most
22
+ * recent call's time, and the ×N badge surfaces how many calls actually ran
23
+ * (the schematic agent fires `ic_search` fourteen times in a row to confirm
24
+ * the part pick; hq-eda-ai shows ONE row for that).
25
+ */
26
+ export interface StackNode {
27
+ /** Stable React key. Path-derived for synthesized nodes, frame id otherwise. */
28
+ id: string
29
+ /** Display name (`node:plan` for graph nodes, bare name for tools). */
30
+ name: string
31
+ status: TraceStatus
32
+ /** 0 for a root; each nesting level adds one. */
33
+ depth: number
34
+ /**
35
+ * Start of the LATEST invocation. For a one-off this is also the start of
36
+ * the only invocation. The card uses this together with `finishedAt` to
37
+ * compute the latest call's duration.
38
+ */
39
+ startedAt: number
40
+ finishedAt?: number
41
+ children: StackNode[]
42
+ /**
43
+ * How many times this exact call ran. `1` for a one-off.
44
+ *
45
+ * Every repeat — whether from a LangGraph `*_TRACE` event with no task id
46
+ * (schematic) or from the AG-UI tool-call lifecycle with a hidden
47
+ * `::task:<id>` (system design) — lands on the same leaf, keyed on the
48
+ * STRIPPED visible path. The card surfaces the count via a `×N` pill.
49
+ */
50
+ repeat: number
51
+ /**
52
+ * The frame this node was built from. `undefined` for synthesized
53
+ * intermediate nodes, which exist only to hold the nesting shape.
54
+ */
55
+ frame?: TraceFrame
56
+ }
57
+
58
+ /**
59
+ * Build the call-stack tree.
60
+ *
61
+ * Path prefixes are shared, so a parent emitted by five different children
62
+ * is one node, not five. Repeated calls of the same tool at the same scope
63
+ * collapse into one row — both the schematic CUSTOM-trace stream (no task
64
+ * ids) and the system-design AG-UI lifecycle (hidden `::task:<id>` in each
65
+ * instance path) end up with one node per logical call.
66
+ */
67
+ export function buildTree(frames: readonly TraceFrame[]): StackNode[] {
68
+ const roots: StackNode[] = []
69
+ const byPath = new Map<string, StackNode>()
70
+
71
+ for (const frame of frames) {
72
+ const segments = frame.path.split('>').map((s) => s.trim()).filter((s) => s.length > 0)
73
+ if (segments.length === 0) {
74
+ continue
75
+ }
76
+
77
+ // 1. Walk every path prefix, creating intermediate nodes as needed.
78
+ // Key the map on the *visible* path (with any `::task:<id>` marker
79
+ // stripped) so repeats land on one node instead of proliferating into
80
+ // a sibling per invocation.
81
+ let parent: StackNode | null = null
82
+ for (let i = 0; i < segments.length; i++) {
83
+ const raw = segments.slice(0, i + 1).join('>')
84
+ const pathId = stripTaskIds(raw)
85
+ let node = byPath.get(pathId)
86
+ if (!node) {
87
+ node = {
88
+ id: `path:${pathId}`,
89
+ name: stripTaskIds(segments[i]!),
90
+ status: 'running',
91
+ depth: i,
92
+ startedAt: frame.startedAt,
93
+ children: [],
94
+ repeat: 1,
95
+ }
96
+ byPath.set(pathId, node)
97
+ if (parent) {
98
+ parent.children.push(node)
99
+ } else {
100
+ roots.push(node)
101
+ }
102
+ }
103
+ parent = node
104
+ }
105
+
106
+ if (!parent) {
107
+ continue
108
+ }
109
+
110
+ // 2. Decorate the leaf with this frame.
111
+ if (!parent.frame) {
112
+ parent.frame = frame
113
+ // Prefer the frame's own display name: for a graph node the last path
114
+ // segment is the bare node name while the frame name is `node:<name>`.
115
+ parent.name = frame.name
116
+ parent.status = frame.status
117
+ parent.startedAt = frame.startedAt
118
+ if (frame.finishedAt !== undefined) {
119
+ parent.finishedAt = frame.finishedAt
120
+ }
121
+ continue
122
+ }
123
+
124
+ // A repeat of a call we already show: bump the counter and let the
125
+ // latest invocation win for status + duration.
126
+ parent.repeat += 1
127
+ parent.frame = frame
128
+ parent.name = frame.name
129
+ parent.status = frame.status
130
+ parent.startedAt = frame.startedAt
131
+ if (frame.finishedAt !== undefined) {
132
+ parent.finishedAt = frame.finishedAt
133
+ } else {
134
+ delete parent.finishedAt
135
+ }
136
+ }
137
+
138
+ return roots
139
+ }
140
+
141
+ /** Count frames by status, for the `done/total` badge on a parent. */
142
+ export function countStatus(node: StackNode): { finished: number; total: number; failed: number } {
143
+ let finished = 0
144
+ let total = 0
145
+ let failed = 0
146
+ const walk = (n: StackNode): void => {
147
+ for (const child of n.children) {
148
+ total += 1
149
+ if (child.status === 'finished') {
150
+ finished += 1
151
+ }
152
+ if (child.status === 'failed') {
153
+ failed += 1
154
+ }
155
+ walk(child)
156
+ }
157
+ }
158
+ walk(node)
159
+ return { finished, total, failed }
160
+ }
161
+
162
+ /**
163
+ * Format a span as `123ms` under a second and `4.2s` above, mirroring the
164
+ * prototype's duration formatter.
165
+ */
166
+ export function formatDuration(ms: number | null | undefined): string {
167
+ if (ms === null || ms === undefined || !Number.isFinite(ms) || ms < 0) {
168
+ return ''
169
+ }
170
+ if (ms < 1000) {
171
+ return `${Math.round(ms)}ms`
172
+ }
173
+ return `${(ms / 1000).toFixed(1)}s`
174
+ }
175
+
176
+ /** Format an elapsed wall-clock span as `m:ss` — used by the run timer. */
177
+ export function formatElapsed(ms: number): string {
178
+ if (!Number.isFinite(ms) || ms < 0) {
179
+ return '0:00'
180
+ }
181
+ const total = Math.floor(ms / 1000)
182
+ const m = Math.floor(total / 60)
183
+ const s = total % 60
184
+ return `${m}:${String(s).padStart(2, '0')}`
185
+ }
package/src/config.ts ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * `@huaqiu/dsh-tool-schematic-gen` — config, agent identity, run-body and
3
+ * filename helpers (pure, dependency-free).
4
+ *
5
+ * Faithful TypeScript port of the `hq-edge` schematic-gen node half, with ONE
6
+ * deliberate change per migration plan review #9: the demo eda.cn account is
7
+ * REMOVED. The account now always comes from the `huaqiuAuth` service
8
+ * (`getUserInfo()` → `x-user-id` / `x-user-token`); there is no baked-in
9
+ * default credential.
10
+ *
11
+ * @module @huaqiu/dsh-tool-schematic-gen
12
+ */
13
+ import { randomUUID } from 'node:crypto'
14
+
15
+ /** CopilotKit `agentId` values the two tools drive. */
16
+ export const agentIds = {
17
+ /** description → final KiCad schematic (`.kicad_sch` files). */
18
+ SCHEMATIC: 'schemagen',
19
+ /** description → module graph → KiCad project zip. */
20
+ SYSTEM: 'modular_circuit',
21
+ } as const
22
+
23
+ /** Default production CopilotKit endpoint (the prod value the reference scripts POST to). */
24
+ export const DEFAULT_COPILOTKIT_URL = 'https://gen.eda.cn/api/copilotkit'
25
+
26
+ /** Default production export-zip endpoint. */
27
+ export const DEFAULT_EXPORT_ZIP_URL = 'https://gen.eda.cn/api/modular_circuit/export-zip'
28
+
29
+ /**
30
+ * Language hint sent to the agent when the caller omits `user_language`.
31
+ *
32
+ * This was a bare `'简体中文'` literal inside `buildRunBody`, which pinned every
33
+ * agent reply to Chinese even for an English UI. The node half has no way to
34
+ * read the host UI locale (the tool is invoked by the model, not the browser),
35
+ * so the value is a named, overridable default instead of an inline literal.
36
+ */
37
+ export const DEFAULT_AGENT_LANGUAGE = '简体中文'
38
+
39
+ /** Resolved runtime config — endpoints only; the account is per-call via auth. */
40
+ export interface SchematicGenConfig {
41
+ copilotkitUrl: string
42
+ exportZipUrl: string
43
+ cookie: string | null
44
+ /** Fallback agent language; see `DEFAULT_AGENT_LANGUAGE`. */
45
+ defaultLanguage: string
46
+ }
47
+
48
+ /** The eda.cn account derived from `huaqiuAuth` per call. */
49
+ export interface EdaAccount {
50
+ userId: string
51
+ userToken: string
52
+ }
53
+
54
+ /**
55
+ * Resolve the production endpoints from env. No credential defaults: the
56
+ * account is never baked in (migration plan review #9).
57
+ */
58
+ export function resolveConfig(env?: Record<string, string | undefined>): SchematicGenConfig {
59
+ const e = env && typeof env === 'object' ? env : {}
60
+ const get = (k: string, d: string) => (typeof e[k] === 'string' && e[k].length > 0 ? e[k]! : d)
61
+ return {
62
+ copilotkitUrl: get('HQ_EDA_COPILOTKIT_URL', DEFAULT_COPILOTKIT_URL),
63
+ exportZipUrl: get('HQ_EDA_EXPORT_ZIP_URL', DEFAULT_EXPORT_ZIP_URL),
64
+ cookie: typeof e['HQ_EDA_COOKIE'] === 'string' && e['HQ_EDA_COOKIE'].length > 0
65
+ ? e['HQ_EDA_COOKIE']
66
+ : null,
67
+ defaultLanguage: get('HQ_EDA_DEFAULT_LANGUAGE', DEFAULT_AGENT_LANGUAGE),
68
+ }
69
+ }
70
+
71
+ /** Build the headers for the CopilotKit SSE POST. */
72
+ export function buildHeaders(config: SchematicGenConfig, account: EdaAccount, threadId: string): Record<string, string> {
73
+ const h: Record<string, string> = {
74
+ accept: 'text/event-stream',
75
+ 'content-type': 'application/json',
76
+ 'x-user-id': account.userId,
77
+ 'x-user-token': account.userToken,
78
+ 'x-thread-id': threadId,
79
+ Referer: 'https://gen.eda.cn/',
80
+ }
81
+ if (config.cookie) h.cookie = config.cookie
82
+ return h
83
+ }
84
+
85
+ /** Build the headers for the export-zip POST (JSON in, zip out). */
86
+ export function buildExportHeaders(config: SchematicGenConfig, account: EdaAccount): Record<string, string> {
87
+ const h: Record<string, string> = {
88
+ 'content-type': 'application/json',
89
+ 'x-user-id': account.userId,
90
+ 'x-user-token': account.userToken,
91
+ Referer: 'https://gen.eda.cn/',
92
+ }
93
+ if (config.cookie) h.cookie = config.cookie
94
+ return h
95
+ }
96
+
97
+ /** Minimal empty state for the schematic agent. */
98
+ export function emptySchematicState(config: SchematicGenConfig, account: EdaAccount, language: string): Record<string, unknown> {
99
+ return {
100
+ user_id: account.userId,
101
+ token: account.userToken,
102
+ commits: [],
103
+ requirement: '',
104
+ architecture: null,
105
+ circuit: {},
106
+ report: null,
107
+ schFiles: [],
108
+ kicadPro: '',
109
+ outProject: '',
110
+ project_achieve_url: '',
111
+ reportOk: false,
112
+ reportStage: '',
113
+ error: '',
114
+ }
115
+ }
116
+
117
+ /** Empty state for the system-design agent. */
118
+ export function emptySystemState(config: SchematicGenConfig, account: EdaAccount, language: string): Record<string, unknown> {
119
+ return {
120
+ design_name: null,
121
+ pending_module_replacement_req: null,
122
+ available_modules: null,
123
+ connection_outdated: true,
124
+ commented_outline: null,
125
+ user_option: null,
126
+ modules_alternatives: null,
127
+ user_id: account.userId,
128
+ token: account.userToken,
129
+ user_language: language,
130
+ user_input: '',
131
+ design_plan: '',
132
+ original_bom_list: [],
133
+ top_block: null,
134
+ search_plan: [],
135
+ bom_list: [],
136
+ module_list: [],
137
+ connect_result: { connections: [] },
138
+ erc_passed: false,
139
+ connection_count: 0,
140
+ pending_bom_updates: [],
141
+ connect_agent_summary: '',
142
+ connect_iteration_history: [],
143
+ bom_exclusion_list: {},
144
+ reflect_retry_count: 0,
145
+ task_completed: false,
146
+ final_report_content: '',
147
+ circuit_url: null,
148
+ module_graph: null,
149
+ kicad_project_zip_url: null,
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Build the CopilotKit `agent/run` body. A FRESH uuid is assigned to both
155
+ * `threadId` and `runId` (and reused for `x-thread-id`) so every call is a
156
+ * one-shot, independent run.
157
+ */
158
+ export function buildRunBody(
159
+ agentId: string,
160
+ description: string,
161
+ config: SchematicGenConfig,
162
+ account: EdaAccount,
163
+ language?: string,
164
+ threadId?: string,
165
+ ): Record<string, unknown> {
166
+ const tid = typeof threadId === 'string' && threadId.length > 0 ? threadId : randomUUID()
167
+ const runId = randomUUID()
168
+ const msgId = randomUUID()
169
+ const lang = typeof language === 'string' && language.length > 0 ? language : config.defaultLanguage
170
+ const state = agentId === agentIds.SCHEMATIC
171
+ ? emptySchematicState(config, account, lang)
172
+ : emptySystemState(config, account, lang)
173
+ return {
174
+ method: 'agent/run',
175
+ params: { agentId },
176
+ body: {
177
+ threadId: tid,
178
+ runId,
179
+ tools: [],
180
+ context: [{
181
+ description: 'Current Module Circuit Design State',
182
+ value: JSON.stringify({ user_language: lang }),
183
+ }],
184
+ forwardedProps: {},
185
+ state,
186
+ messages: [{ id: msgId, role: 'user', content: String(description || '') }],
187
+ },
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Derive a human-readable, filesystem-safe zip basename from the design name.
193
+ * Keeps Unicode letters/digits (CJK included) intact; only illegal path chars
194
+ * and whitespace are replaced.
195
+ */
196
+ export function sanitizeZipBaseName(designName: string): string {
197
+ return String(designName || '')
198
+ .replace(/[\\/:*?"<>|\u0000-\u001f]+/g, ' ')
199
+ .replace(/\s+/g, '_')
200
+ .replace(/_+/g, '_')
201
+ .replace(/^_+|_+$/g, '')
202
+ .slice(0, 60) || 'circuit'
203
+ }