@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
|
@@ -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
|
+
}
|