@huaqiu/dsh-hqsch 0.4.8

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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -0
  3. package/cordis.patch.yml +13 -0
  4. package/lib/index.d.mts +98 -0
  5. package/lib/index.mjs +159 -0
  6. package/package.json +40 -0
  7. package/skills/hqsch-scenario-b/AGENTS.md +209 -0
  8. package/skills/hqsch-scenario-b/CLAUDE.md +209 -0
  9. package/skills/hqsch-scenario-b/INSTALL.md +140 -0
  10. package/skills/hqsch-scenario-b/SKILL.md +107 -0
  11. package/skills/hqsch-scenario-b/SYSTEM-PROMPT.md +209 -0
  12. package/skills/hqsch-scenario-b/docs/circuit-pattern-layout.md +304 -0
  13. package/skills/hqsch-scenario-b/docs/editing-a-circuit.md +167 -0
  14. package/skills/hqsch-scenario-b/docs/part-search.md +244 -0
  15. package/skills/hqsch-scenario-b/docs/placement-conventions.md +714 -0
  16. package/skills/hqsch-scenario-b/docs/property-conventions.md +225 -0
  17. package/skills/hqsch-scenario-b/docs/quickstart.md +99 -0
  18. package/skills/hqsch-scenario-b/docs/reading-a-circuit.md +317 -0
  19. package/skills/hqsch-scenario-b/docs/rpc/canvas-auto-connect-objects-by-id.md +98 -0
  20. package/skills/hqsch-scenario-b/docs/rpc/canvas-delete-objects-by-ids.md +94 -0
  21. package/skills/hqsch-scenario-b/docs/rpc/canvas-find-object-by-property.md +101 -0
  22. package/skills/hqsch-scenario-b/docs/rpc/canvas-list-wire-segments.md +94 -0
  23. package/skills/hqsch-scenario-b/docs/rpc/canvas-mirror-objects-by-ids.md +96 -0
  24. package/skills/hqsch-scenario-b/docs/rpc/canvas-rotate-objects-by-ids.md +94 -0
  25. package/skills/hqsch-scenario-b/docs/rpc/canvas-set-object-property.md +99 -0
  26. package/skills/hqsch-scenario-b/docs/rpc/canvas-zoom-all.md +92 -0
  27. package/skills/hqsch-scenario-b/docs/rpc/find-find-object.md +96 -0
  28. package/skills/hqsch-scenario-b/docs/rpc/kernel-get-snapshot.md +104 -0
  29. package/skills/hqsch-scenario-b/docs/rpc/netlist-get-active-page-net-list.md +87 -0
  30. package/skills/hqsch-scenario-b/docs/rpc/netlist-get-port-pairing.md +34 -0
  31. package/skills/hqsch-scenario-b/docs/rpc/obj-place-place-pin-stub-wire-and-net-alias.md +114 -0
  32. package/skills/hqsch-scenario-b/docs/rpc/pattern-layout-apply-circuit-pattern.md +125 -0
  33. package/skills/hqsch-scenario-b/docs/rpc/pattern-layout-get-page-occupancy.md +100 -0
  34. package/skills/hqsch-scenario-b/docs/rpc/pattern-layout-list-circuit-patterns.md +97 -0
  35. package/skills/hqsch-scenario-b/docs/rpc/placement-list-part-libraries.md +94 -0
  36. package/skills/hqsch-scenario-b/docs/rpc/placement-list-symbol-libraries.md +94 -0
  37. package/skills/hqsch-scenario-b/docs/rpc/placement-place-kicad-symbol.md +105 -0
  38. package/skills/hqsch-scenario-b/docs/rpc/placement-place-part-from-library.md +106 -0
  39. package/skills/hqsch-scenario-b/docs/rpc/placement-place-symbol-from-library.md +108 -0
  40. package/skills/hqsch-scenario-b/docs/rpc/project-get-active-project.md +96 -0
  41. package/skills/hqsch-scenario-b/docs/rpc/project-list-open-projects.md +99 -0
  42. package/skills/hqsch-scenario-b/docs/rpc-availability.md +97 -0
  43. package/skills/hqsch-scenario-b/docs/rpc-index/canvas.md +81 -0
  44. package/skills/hqsch-scenario-b/docs/rpc-index/find.md +36 -0
  45. package/skills/hqsch-scenario-b/docs/rpc-index/kernel.md +36 -0
  46. package/skills/hqsch-scenario-b/docs/rpc-index/netlist.md +38 -0
  47. package/skills/hqsch-scenario-b/docs/rpc-index/obj-place.md +76 -0
  48. package/skills/hqsch-scenario-b/docs/rpc-index/pattern-layout.md +38 -0
  49. package/skills/hqsch-scenario-b/docs/rpc-index/placement.md +48 -0
  50. package/skills/hqsch-scenario-b/docs/rpc-index/project.md +47 -0
  51. package/skills/hqsch-scenario-b/docs/serialization.md +60 -0
  52. package/skills/hqsch-scenario-b/docs/skill-dependencies.md +93 -0
  53. package/skills/hqsch-scenario-b/skill.json +13 -0
  54. package/skills/hqsch-scenario-b/template/package.json +16 -0
  55. package/skills/hqsch-scenario-b/template/scripts/edit-property-by-ref.ts +44 -0
  56. package/skills/hqsch-scenario-b/template/scripts/fix-designators.ts +52 -0
  57. package/skills/hqsch-scenario-b/template/scripts/hello.ts +46 -0
  58. package/skills/hqsch-scenario-b/template/scripts/read-circuit.ts +357 -0
  59. package/skills/hqsch-scenario-b/template/scripts/replace-part-by-ref.ts +44 -0
  60. package/skills/hqsch-scenario-b/template/scripts/rewire-pin-by-ref.ts +42 -0
  61. package/skills/hqsch-scenario-b/template/tsconfig.json +11 -0
  62. package/src/config.ts +25 -0
  63. package/src/index.ts +186 -0
  64. package/src/paths.ts +48 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * 读电路模板 —— 理解已有设计的标准流程(只读,不修改任何东西)
3
+ *
4
+ * 用法:
5
+ * npx tsx scripts/read-circuit.ts
6
+ * REF=VR201 npx tsx scripts/read-circuit.ts
7
+ * NET=+3.3V npx tsx scripts/read-circuit.ts
8
+ * REF=VR201 TRACE=1 npx tsx scripts/read-circuit.ts
9
+ */
10
+ import { connect } from "@huaqiu/huaqiu-client";
11
+
12
+ const INSTANCE = process.env.HQ_INSTANCE_ID;
13
+ const REF = process.env.REF;
14
+ const NET = process.env.NET;
15
+ const TRACE = process.env.TRACE === "1" || process.env.TRACE === "true";
16
+ const TRACE_DEPTH = Math.max(1, Math.min(4, parseInt(process.env.TRACE_DEPTH ?? "2", 10) || 2));
17
+
18
+ const bigintSafe = (_k: string, v: unknown) => (typeof v === "bigint" ? String(v) : v);
19
+ const js = (v: unknown, n = 400) => {
20
+ const s = JSON.stringify(v, bigintSafe, 1) ?? "(undefined)";
21
+ return s.length > n ? s.slice(0, n) + " …" : s;
22
+ };
23
+ const pos = (v: unknown) => {
24
+ const o = v as { x?: unknown; y?: unknown } | undefined;
25
+ return o && o.x !== undefined ? `(${o.x},${o.y})` : js(v, 60);
26
+ };
27
+ function propVal(v: unknown): string {
28
+ if (v === null || v === undefined) return "";
29
+ if (typeof v === "object") {
30
+ const o = v as Record<string, unknown>;
31
+ return "value" in o ? propVal(o.value) : js(v, 120);
32
+ }
33
+ return String(v);
34
+ }
35
+ const isRailName = (n: string) =>
36
+ /^[+-]?\d+(\.\d+)?V$/i.test(n) || /^V(CC|DD|EE|BAT|IN|OUT)?[A-Z0-9_]*$/i.test(n) || /^\+/.test(n);
37
+
38
+ type Sym = Record<string, unknown>;
39
+ type Pin = Record<string, unknown>;
40
+ type Net = { name?: string; netClass?: number; pinInstanceIds?: string[] };
41
+ type Def = { name?: string; pins?: Array<{ number?: string; name?: string; electricalType?: number }> };
42
+ type PinRow = { num: string; name: string; net: string; pos: unknown };
43
+
44
+ const NET_CLASS: Record<number, string> = { 0: "UNSPECIFIED", 1: "SIGNAL", 2: "POWER", 3: "GROUND", 4: "BUS" };
45
+
46
+ function metaId(obj: Record<string, unknown> | undefined): string {
47
+ return String((obj?.metadata as Record<string, unknown> | undefined)?.id ?? "");
48
+ }
49
+ function symMetaId(sym: Sym): string {
50
+ return metaId(sym as Record<string, unknown>) || String(sym.canvasObjectId ?? "");
51
+ }
52
+ function pinMetaId(pin: Pin): string {
53
+ return metaId(pin as Record<string, unknown>);
54
+ }
55
+ function refPrefix(ref: string): string {
56
+ const m = /^([A-Za-z_]+)/.exec(ref);
57
+ return m?.[1] ?? ref;
58
+ }
59
+ function isPassiveBridgePrefix(ref: string): boolean {
60
+ return /^(C|R|L|FB|FL|BEAD)$/i.test(refPrefix(ref));
61
+ }
62
+ function isActivePrefix(ref: string): boolean {
63
+ const p = refPrefix(ref).toUpperCase();
64
+ return /^(U|VR|Q|IC|D|J|X|Y|K|SW|S|T|M)/.test(p) && !isPassiveBridgePrefix(ref);
65
+ }
66
+ function isPowerRailNet(name: string, netClass?: number): boolean {
67
+ if ((netClass ?? 0) >= 2) return true;
68
+ return /^(GND|VCC|VDD|VEE|VSS|\+|\-)/i.test(name) || isRailName(name);
69
+ }
70
+ function neighborRoleHint(ref: string): string {
71
+ const p = refPrefix(ref).toUpperCase();
72
+ if (/^C/.test(p)) return "旁路/滤波电容";
73
+ if (/^R/.test(p)) return "电阻(上拉/下拉/分压)";
74
+ if (/^(L|FB|FL|BEAD)/.test(p)) return "电感/磁珠";
75
+ if (/^(U|VR|IC)/.test(p)) return "有源器件";
76
+ if (/^(D|Q)/.test(p)) return "二极管/晶体管";
77
+ if (/^(VCC|VDD|GND)/.test(ref)) return "电源轨符号";
78
+ return "无源/其它";
79
+ }
80
+ function symValue(sym: Sym | undefined): string {
81
+ if (!sym) return "";
82
+ const props = ((sym.metadata as Record<string, unknown> | undefined)?.properties as Array<{ key: string; value: unknown }>) ?? [];
83
+ const v = props.find((p) => p.key === "Value");
84
+ return v ? propVal(v.value).slice(0, 40) : "";
85
+ }
86
+
87
+ class SnapCtx {
88
+ readonly syms: Sym[];
89
+ readonly pins: Pin[];
90
+ readonly nets: Net[];
91
+ readonly defs: Def[];
92
+ readonly netOf = new Map<string, string>();
93
+ readonly pinById = new Map<string, Pin>();
94
+ readonly symByMetaId = new Map<string, Sym>();
95
+ readonly symByDesignator = new Map<string, Sym>();
96
+ readonly defByName = new Map<string, Def>();
97
+ readonly netByName = new Map<string, Net>();
98
+
99
+ constructor(raw: Record<string, unknown[]>) {
100
+ this.syms = (raw.symbolInstances ?? []) as Sym[];
101
+ this.pins = (raw.pinInstances ?? []) as Pin[];
102
+ this.nets = (raw.nets ?? []) as unknown as Net[];
103
+ this.defs = (raw.symbolDefinitions ?? []) as unknown as Def[];
104
+ for (const net of this.nets) {
105
+ if (net.name) this.netByName.set(net.name, net);
106
+ for (const pid of net.pinInstanceIds ?? []) this.netOf.set(pid, net.name ?? "?");
107
+ }
108
+ for (const p of this.pins) this.pinById.set(pinMetaId(p), p);
109
+ for (const s of this.syms) {
110
+ if (s.designator) this.symByDesignator.set(String(s.designator), s);
111
+ this.symByMetaId.set(symMetaId(s), s);
112
+ }
113
+ for (const d of this.defs) if (d.name) this.defByName.set(d.name, d);
114
+ }
115
+
116
+ pinNameTable(definitionId: unknown): Map<string, string> {
117
+ const d = this.defByName.get(String(definitionId));
118
+ const m = new Map<string, string>();
119
+ for (const p of d?.pins ?? []) m.set(String(p.number), String(p.name));
120
+ return m;
121
+ }
122
+ pinsOfRef(ref: string): Pin[] {
123
+ const sym = this.symByDesignator.get(ref);
124
+ if (!sym) return [];
125
+ return this.pins.filter((p) => String(p.symbolInstanceId) === symMetaId(sym));
126
+ }
127
+ pinRows(ref: string): PinRow[] {
128
+ const sym = this.symByDesignator.get(ref);
129
+ if (!sym) return [];
130
+ const pn = this.pinNameTable(sym.definitionId);
131
+ return this.pinsOfRef(ref)
132
+ .map((p) => ({
133
+ num: String(p.pinDefinitionId),
134
+ name: pn.get(String(p.pinDefinitionId)) ?? "?",
135
+ net: this.netOf.get(pinMetaId(p)) ?? "(未连接)",
136
+ pos: p.position,
137
+ }))
138
+ .sort((a, b) => Number(a.num) - Number(b.num));
139
+ }
140
+ netMembers(netName: string, excludeRef?: string): Array<{ ref: string; pinLabel: string }> {
141
+ const net = this.netByName.get(netName);
142
+ if (!net) return [];
143
+ const out: Array<{ ref: string; pinLabel: string }> = [];
144
+ for (const pid of net.pinInstanceIds ?? []) {
145
+ const p = this.pinById.get(pid);
146
+ if (!p) continue;
147
+ const sym = this.symByMetaId.get(String(p.symbolInstanceId));
148
+ const ref = String(sym?.designator ?? "?");
149
+ if (excludeRef && ref === excludeRef) continue;
150
+ const pn = this.pinNameTable(sym?.definitionId);
151
+ const num = String(p.pinDefinitionId);
152
+ out.push({ ref, pinLabel: `${num}(${pn.get(num) ?? "?"})` });
153
+ }
154
+ return out;
155
+ }
156
+ prefixStats(members: Array<{ ref: string }>): string {
157
+ const cnt = new Map<string, number>();
158
+ const refs = new Set<string>();
159
+ for (const m of members) {
160
+ if (refs.has(m.ref)) continue;
161
+ refs.add(m.ref);
162
+ cnt.set(refPrefix(m.ref), (cnt.get(refPrefix(m.ref)) ?? 0) + 1);
163
+ }
164
+ return [...cnt.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}×${v}`).join(", ");
165
+ }
166
+ }
167
+
168
+ type SeriesBridge = { ref: string; value: string; netA: string; netB: string };
169
+
170
+ function buildSeriesBridgeGraph(ctx: SnapCtx): SeriesBridge[] {
171
+ const bridges: SeriesBridge[] = [];
172
+ const seen = new Set<string>();
173
+ for (const sym of ctx.syms) {
174
+ const ref = String(sym.designator ?? "");
175
+ if (!ref || !isPassiveBridgePrefix(ref)) continue;
176
+ const myPins = ctx.pinsOfRef(ref).filter((p) => ctx.netOf.get(pinMetaId(p)));
177
+ if (myPins.length !== 2) continue;
178
+ const nets = [...new Set(myPins.map((p) => ctx.netOf.get(pinMetaId(p))!))];
179
+ if (nets.length !== 2) continue;
180
+ const [netA, netB] = nets;
181
+ if (refPrefix(ref).toUpperCase() === "C") {
182
+ const aRail = isPowerRailNet(netA, ctx.netByName.get(netA)?.netClass);
183
+ const bRail = isPowerRailNet(netB, ctx.netByName.get(netB)?.netClass);
184
+ if (aRail !== bRail) continue;
185
+ }
186
+ const key = [ref, ...nets.sort()].join("|");
187
+ if (seen.has(key)) continue;
188
+ seen.add(key);
189
+ bridges.push({ ref, value: symValue(sym), netA, netB });
190
+ }
191
+ return bridges;
192
+ }
193
+ function bridgesFromNet(bridges: SeriesBridge[], netName: string): SeriesBridge[] {
194
+ return bridges.filter((b) => b.netA === netName || b.netB === netName);
195
+ }
196
+ function otherNet(bridge: SeriesBridge, netName: string): string {
197
+ return bridge.netA === netName ? bridge.netB : bridge.netA;
198
+ }
199
+
200
+ function expandNeighborhood(ctx: SnapCtx, ref: string, rows: PinRow[]): void {
201
+ console.log(`\n ── 邻域展开(同网 1 跳)──`);
202
+ for (const r of rows) {
203
+ if (r.net === "(未连接)") {
204
+ console.log(` pin${r.num} ${r.name.padEnd(10)} → (未连接)`);
205
+ continue;
206
+ }
207
+ const allMembers = ctx.netMembers(r.net);
208
+ const neighbors = ctx.netMembers(r.net, ref);
209
+ const net = ctx.netByName.get(r.net);
210
+ const nc = NET_CLASS[net?.netClass ?? -1] ?? String(net?.netClass ?? "?");
211
+ console.log(` pin${r.num} ${String(r.name).padEnd(10)} → ${String(r.net).padEnd(16)} [${nc}]`);
212
+ if (allMembers.length <= 8) {
213
+ const list = neighbors.map((m) => `${m.ref}(${m.pinLabel})`).join(", ");
214
+ console.log(` 同网邻居: ${list || "(仅本器件)"} [${new Set(allMembers.map((m) => m.ref)).size} 器件 / ${neighbors.length} 脚]`);
215
+ } else {
216
+ console.log(` 同网邻居: (大网摘要) ${new Set(allMembers.map((m) => m.ref)).size} 器件 / ${allMembers.length} 脚 | ${ctx.prefixStats(neighbors)}`);
217
+ console.log(` 代表: ${neighbors.slice(0, 5).map((m) => `${m.ref}(${m.pinLabel})`).join(", ")}${neighbors.length > 5 ? " …" : ""}`);
218
+ }
219
+ const roles = [...new Set(neighbors.slice(0, 6).map((m) => neighborRoleHint(m.ref)))];
220
+ if (roles.length) console.log(` 角色推断: ${roles.join(";")}`);
221
+ for (const m of neighbors.slice(0, 4)) {
222
+ const val = symValue(ctx.symByDesignator.get(m.ref));
223
+ if (val) console.log(` ${m.ref} Value=${val}`);
224
+ }
225
+ }
226
+ }
227
+
228
+ function traceThroughPassives(ctx: SnapCtx, ref: string, rows: PinRow[], bridges: SeriesBridge[], maxDepth: number): void {
229
+ console.log(`\n ── 串联穿透(C/R/L/FB,max ${maxDepth} 跳)──`);
230
+ let found = 0;
231
+ for (const r of rows) {
232
+ if (r.net === "(未连接)") continue;
233
+ type QItem = { net: string; path: string[]; depth: number };
234
+ const queue: QItem[] = [{ net: r.net, path: [r.net], depth: 0 }];
235
+ const visited = new Set<string>();
236
+ while (queue.length) {
237
+ const { net, path, depth } = queue.shift()!;
238
+ if (depth >= maxDepth) continue;
239
+ for (const br of bridgesFromNet(bridges, net)) {
240
+ const next = otherNet(br, net);
241
+ if (path.includes(next)) continue;
242
+ if (isPowerRailNet(next, ctx.netByName.get(next)?.netClass) && depth > 0) continue;
243
+ const label = br.value ? `${br.ref} ${br.value}` : br.ref;
244
+ console.log(` pin${r.num} ${r.name}: ${path.join(" → ")} —[${label}]— ${next}`);
245
+ const active = ctx.netMembers(next, ref).filter((m) => isActivePrefix(m.ref));
246
+ if (active.length) console.log(` 对侧有源: ${active.slice(0, 5).map((m) => `${m.ref}(${m.pinLabel})`).join(", ")}`);
247
+ else {
248
+ const passive = ctx.netMembers(next, ref).slice(0, 4).map((m) => `${m.ref}(${m.pinLabel})`).join(", ");
249
+ if (passive) console.log(` 对侧: ${passive}`);
250
+ }
251
+ found++;
252
+ const vkey = `${next}|${depth + 1}`;
253
+ if (!visited.has(vkey)) {
254
+ visited.add(vkey);
255
+ queue.push({ net: next, path: [...path, next], depth: depth + 1 });
256
+ }
257
+ }
258
+ }
259
+ }
260
+ if (!found) console.log(` (未发现可穿透的串联 C/R/L/FB 链路)`);
261
+ }
262
+
263
+ function printRoleSummary(ref: string, sym: Sym, rows: PinRow[], props: Array<{ key: string; value: unknown }>): void {
264
+ console.log(`\n ── 作用回答要点(供 agent 组织自然语言)──`);
265
+ const desc = props.find((p) => p.key === "Description");
266
+ const mpn = props.find((p) => p.key === "MPN");
267
+ if (desc) console.log(` 身份: ${propVal(desc.value).slice(0, 120)}`);
268
+ else console.log(` 身份: ${String(sym.definitionId)}(无 Description,勿编造 datasheet)`);
269
+ if (mpn) console.log(` MPN: ${propVal(mpn.value)}`);
270
+ console.log(` 连接: ${rows.map((r) => `${r.name}→${r.net}`).join("; ")}`);
271
+ console.log(` 总结: 结合邻域${TRACE ? "/穿透" : ""}块描述 ${ref} 在局部子电路中的功能`);
272
+ }
273
+
274
+ async function resolveCanvasIds(
275
+ client: Awaited<ReturnType<typeof connect>>,
276
+ ctx: ReturnType<Awaited<ReturnType<typeof connect>>["createProjectContext"]>,
277
+ ref: string,
278
+ sym: Sym,
279
+ ): Promise<void> {
280
+ console.log(`\n ── canvas 编辑 id(读懂 → 改电路桥接)──`);
281
+ console.log(` snapshot metadata.id: ${symMetaId(sym)}`);
282
+ console.log(` canvasObjectId: ${sym.canvasObjectId ?? "(空)"}`);
283
+ try {
284
+ const found = await client.canvasOps.findObjectByProperty({ context: ctx, propKey: "Reference", propValue: ref });
285
+ console.log(` FindObject(Reference): ${found.objectIds?.[0] ?? "(未找到)"}`);
286
+ } catch {
287
+ console.log(` FindObject(Reference): (调用失败)`);
288
+ }
289
+ }
290
+
291
+ async function main() {
292
+ const client = INSTANCE ? await connect({ instanceId: INSTANCE }) : await connect();
293
+ const active = await client.project.getActiveProject({ context: client.createEditorContext() });
294
+ const projectId = active.project?.projectId;
295
+ if (!projectId) throw new Error("请先打开原理图工程");
296
+ const pctx = client.createProjectContext(projectId);
297
+ console.log(`工程 projectId: ${projectId}\n`);
298
+
299
+ const t = Date.now();
300
+ const snap = await client.kernel.getSnapshot({ context: pctx });
301
+ const ctx = new SnapCtx(snap.snapshot as unknown as Record<string, unknown[]>);
302
+ console.log(`[快照] ${Date.now() - t}ms — ${ctx.syms.length} 器件 / ${ctx.pins.length} 引脚 / ${ctx.nets.length} 网络\n`);
303
+
304
+ if (!REF && !NET) {
305
+ console.log("=== 网络分类(netClass)===");
306
+ const byClass = new Map<number, number>();
307
+ for (const n of ctx.nets) byClass.set(n.netClass ?? -1, (byClass.get(n.netClass ?? -1) ?? 0) + 1);
308
+ for (const [k, v] of [...byClass.entries()].sort()) console.log(` ${(NET_CLASS[k] ?? k).padEnd(12)} ${v}`);
309
+ const bridges = buildSeriesBridgeGraph(ctx);
310
+ console.log(`\n=== 可穿透串联无源件: ${bridges.length} 个 ===`);
311
+ for (const b of bridges.slice(0, 8)) console.log(` ${b.ref} ${b.netA} ↔ ${b.netB}`);
312
+ console.log(`\n提示:REF= / NET= ;串联穿透 TRACE=1`);
313
+ return;
314
+ }
315
+
316
+ if (REF) {
317
+ const sym = ctx.symByDesignator.get(REF);
318
+ if (!sym) {
319
+ const cands = ctx.syms.filter((x) => String(x.designator ?? "").includes(REF));
320
+ console.log(`未找到位号 ${REF}`);
321
+ if (cands.length) console.log(` 相近: ${cands.slice(0, 8).map((x) => x.designator).join(", ")}`);
322
+ return;
323
+ }
324
+ console.log(`=== ${REF} ===`);
325
+ console.log(` definitionId: ${sym.definitionId} position: ${pos(sym.position)}`);
326
+ const props = ((sym.metadata as Record<string, unknown> | undefined)?.properties as Array<{ key: string; value: unknown }>) ?? [];
327
+ if (props.filter((p) => p.key !== "pins").length) {
328
+ console.log(`\n ── 器件参数 ──`);
329
+ for (const p of props.filter((x) => x.key !== "pins")) console.log(` ${p.key} = ${propVal(p.value).slice(0, 100)}`);
330
+ } else console.log(`\n ── 器件参数 ── (空) 禁止编造 datasheet`);
331
+ const rows = ctx.pinRows(REF);
332
+ console.log(`\n ── 引脚 → 网络 ──`);
333
+ for (const r of rows) console.log(` pin${r.num} ${String(r.name).padEnd(10)} → ${r.net}`);
334
+ expandNeighborhood(ctx, REF, rows);
335
+ const bridges = buildSeriesBridgeGraph(ctx);
336
+ if (TRACE) traceThroughPassives(ctx, REF, rows, bridges, TRACE_DEPTH);
337
+ else console.log(`\n (设 TRACE=1 开启串联穿透)`);
338
+ printRoleSummary(REF, sym, rows, props);
339
+ await resolveCanvasIds(client, pctx, REF, sym);
340
+ }
341
+
342
+ if (NET) {
343
+ const net = ctx.netByName.get(NET);
344
+ if (!net) { console.log(`未找到网络 ${NET}`); return; }
345
+ console.log(`\n=== 网络 "${NET}" === netClass=${net.netClass} 成员=${net.pinInstanceIds?.length ?? 0} 脚`);
346
+ const byRef = new Map<string, string[]>();
347
+ for (const m of ctx.netMembers(NET)) {
348
+ if (!byRef.has(m.ref)) byRef.set(m.ref, []);
349
+ byRef.get(m.ref)!.push(m.pinLabel);
350
+ }
351
+ for (const [ref, list] of [...byRef.entries()].sort()) console.log(` ${ref}: ${list.join(", ")} (${neighborRoleHint(ref)})`);
352
+ for (const b of bridgesFromNet(buildSeriesBridgeGraph(ctx), NET))
353
+ console.log(` 串联: ${b.ref} → 对侧 ${otherNet(b, NET)}`);
354
+ }
355
+ }
356
+
357
+ main().catch((e) => { console.error("❌", e.message); process.exit(1); });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Flow C 配方:删旧放新(骨架 — 需补 part-search 与 pin 重连列表)
3
+ * OLD_REF=U3 CONFIRM=1 npx tsx scripts/replace-part-by-ref.ts
4
+ *
5
+ * 完整替换需:searchParts → placeKicadSymbol → 按 OLD 的 pin→net 逐脚 autoConnect
6
+ */
7
+ import { connect } from "@huaqiu/huaqiu-client";
8
+
9
+ const OLD_REF = process.env.OLD_REF;
10
+ const CONFIRM = process.env.CONFIRM === "1";
11
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
12
+
13
+ async function main() {
14
+ if (!OLD_REF) throw new Error("需要 OLD_REF=");
15
+ const client = await connect({ instanceId: process.env.HQ_INSTANCE_ID });
16
+ const active = await client.project.getActiveProject({ context: client.createEditorContext() });
17
+ const projectId = active.project?.projectId;
18
+ if (!projectId) throw new Error("请先打开工程");
19
+ const ctx = client.createProjectContext(projectId);
20
+ console.log("projectId:", projectId);
21
+
22
+ const snap = await client.kernel.getSnapshot({ context: ctx });
23
+ const s = snap.snapshot as Record<string, unknown[]>;
24
+ const sym = (s.symbolInstances as Array<Record<string, unknown>>)?.find((x) => x.designator === OLD_REF);
25
+ if (!sym) throw new Error(`快照无 ${OLD_REF}`);
26
+
27
+ console.log(`\n计划: 删除 ${OLD_REF} @ ${JSON.stringify(sym.position)} rotation=${sym.rotation}`);
28
+ console.log(" ⚠ 替换需指定 NEW_MPN + placeKicadSymbol + 重连 — 本脚本仅演示定位与删除前检查");
29
+ if (!CONFIRM) { console.log("未执行。设 CONFIRM=1 且补全新器件放置逻辑。"); return; }
30
+
31
+ const found = await client.canvasOps.findObjectByProperty({ context: ctx, propKey: "Reference", propValue: OLD_REF });
32
+ const id = found.objectIds?.[0];
33
+ if (!id) throw new Error("FindObject 失败");
34
+ await client.canvasOps.deleteObjectsByIds({ context: ctx, objectIds: [id] });
35
+ for (let i = 0; i < 10; i++) {
36
+ await sleep(300);
37
+ const occ = await client.canvasOps.getPageOccupancy({ context: ctx });
38
+ const ids = (occ.objectIds ?? []).map(String);
39
+ if (!ids.includes(String(id))) break;
40
+ }
41
+ console.log(`✓ 已删除 ${OLD_REF} — 请 placeKicadSymbol 并 autoConnect 各 pin`);
42
+ }
43
+
44
+ main().catch((e) => { console.error("❌", e.message); process.exit(1); });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Flow C 配方:把 REF 的某 pin 接到 TARGET_REF 的 TARGET_PIN(autoConnect)
3
+ * REF=R201 PIN=1 TARGET_REF=U301 TARGET_PIN=14 CONFIRM=1 npx tsx scripts/rewire-pin-by-ref.ts
4
+ */
5
+ import { connect } from "@huaqiu/huaqiu-client";
6
+
7
+ const REF = process.env.REF;
8
+ const PIN = process.env.PIN;
9
+ const TARGET_REF = process.env.TARGET_REF;
10
+ const TARGET_PIN = process.env.TARGET_PIN ?? "1";
11
+ const CONFIRM = process.env.CONFIRM === "1";
12
+
13
+ async function main() {
14
+ if (!REF || !PIN || !TARGET_REF) throw new Error("需要 REF= PIN= TARGET_REF=");
15
+ const client = await connect({ instanceId: process.env.HQ_INSTANCE_ID });
16
+ const active = await client.project.getActiveProject({ context: client.createEditorContext() });
17
+ const projectId = active.project?.projectId;
18
+ if (!projectId) throw new Error("请先打开工程");
19
+ const ctx = client.createProjectContext(projectId);
20
+ console.log("projectId:", projectId);
21
+
22
+ const a = await client.canvasOps.findObjectByProperty({ context: ctx, propKey: "Reference", propValue: REF });
23
+ const b = await client.canvasOps.findObjectByProperty({ context: ctx, propKey: "Reference", propValue: TARGET_REF });
24
+ const id1 = a.objectIds?.[0];
25
+ const id2 = b.objectIds?.[0];
26
+ if (!id1 || !id2) throw new Error("找不到 objectId");
27
+
28
+ console.log(`\n计划: ${REF}.pin${PIN} autoConnect → ${TARGET_REF}.pin${TARGET_PIN}`);
29
+ console.log(` id1=${id1} id2=${id2}`);
30
+ if (!CONFIRM) { console.log("未执行。设 CONFIRM=1"); return; }
31
+
32
+ await client.canvasOps.autoConnectObjectsById({
33
+ context: ctx,
34
+ objectId1: id1,
35
+ pinNum1: [Number(PIN)],
36
+ objectId2: id2,
37
+ pinNum2: [Number(TARGET_PIN)],
38
+ });
39
+ console.log("✓ autoConnect 完成 — 请 GetSnapshot 验收 pin→net");
40
+ }
41
+
42
+ main().catch((e) => { console.error("❌", e.message); process.exit(1); });
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true
9
+ },
10
+ "include": ["scripts/**/*.ts"]
11
+ }
package/src/config.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Configuration for `@huaqiu/dsh-hqsch`.
3
+ *
4
+ * This plugin only bundles and registers the HQSCH scenario-b skill. The skill
5
+ * itself guides the agent to generate and run local TypeScript scripts against
6
+ * HQ EDA, so there is no host connection configured here.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ /** Canonical skill directory name shipped by this package. */
12
+ export const HQSCH_SKILL_NAME = 'hqsch-scenario-b'
13
+
14
+ /** Configuration accepted by the plugin's `apply()`. */
15
+ export interface HqschConfig {
16
+ /** Bundled skill directory override (defaults to this package's skills/). */
17
+ skillsDir?: string
18
+ }
19
+
20
+ export type HqschConfigInput = Partial<HqschConfig>
21
+
22
+ /** Whether any host configuration was supplied (used for startup logging). */
23
+ export function hasHostConfig(input: HqschConfigInput = {}): boolean {
24
+ return Boolean(input.skillsDir ?? process.env['DSH_HQSCH_SKILLS_DIR'])
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * `@huaqiu/dsh-hqsch` - node plugin entry.
3
+ *
4
+ * This plugin ships the HQSCH scenario-b skill as a DSH bundled skill. It does
5
+ * not register DSH tools: scenario-b's execution model is for the coding agent
6
+ * to generate TypeScript scripts under the bundled template and run them
7
+ * locally against HQ EDA.
8
+ *
9
+ * @module @huaqiu/dsh-hqsch
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis'
12
+ import { getLogger } from '@huaqiu/dsh-plugin-log'
13
+ import { readFileSync } from 'node:fs'
14
+ import { join } from 'node:path'
15
+
16
+ import {
17
+ hasHostConfig,
18
+ HQSCH_SKILL_NAME,
19
+ type HqschConfig,
20
+ type HqschConfigInput,
21
+ } from './config.js'
22
+ import { requireSkillDir, resolveSkillDir } from './paths.js'
23
+
24
+ /** Plugin id - matches package.json. */
25
+ export const name = '@huaqiu/dsh-hqsch'
26
+
27
+ /**
28
+ * `skills` is REQUIRED: it is the DSH runtime's plugin-bundled skill registry.
29
+ */
30
+ export const inject = ['skills'] as const
31
+
32
+ export type { HqschConfig, HqschConfigInput } from './config.js'
33
+ export { HQSCH_SKILL_NAME } from './config.js'
34
+ export { requireSkillDir, resolveSkillDir } from './paths.js'
35
+
36
+ /** The `skills` service shape consumed by this plugin. */
37
+ export interface SkillRegistration {
38
+ /** Skill id - must match DSH's discoverable skill id format. */
39
+ name: string
40
+ /** Catalog description shown to the model. */
41
+ description: string
42
+ /** Full SKILL.md body. */
43
+ content: string
44
+ /** Discovery source for plugin-bundled skills. */
45
+ source: 'runtime'
46
+ /** Where docs/ and template/ live for progressive disclosure. */
47
+ resourceBase: { kind: 'directory'; path: string }
48
+ }
49
+
50
+ declare module '@deepseek-ai/cordis' {
51
+ interface Context {
52
+ /** DSH skill registry, provided by the runtime skill service. */
53
+ skills: {
54
+ /** Register a plugin-bundled skill; returns its unregister disposer. */
55
+ register(skill: SkillRegistration): () => void
56
+ }
57
+ }
58
+ }
59
+
60
+ const COMPONENT = 'dsh-hqsch'
61
+ const log = getLogger(COMPONENT)
62
+
63
+ log.info('dsh-hqsch: module loaded (waiting for the skills service)')
64
+
65
+ const FALLBACK_SKILL_DESCRIPTION =
66
+ 'HQSCH scenario-b automation skill for HQ EDA schematic drawing, reading and ' +
67
+ 'local surgical edits through generated TypeScript scripts.'
68
+
69
+ /**
70
+ * Extract the `description` field from SKILL.md YAML frontmatter.
71
+ *
72
+ * Supports both single-line values and the folded block form used by
73
+ * hqeda-scenario-b (`description: >-` followed by indented lines).
74
+ */
75
+ export function skillDescription(markdown: string): string | undefined {
76
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown)
77
+ if (!frontmatter?.[1]) return undefined
78
+
79
+ const lines = frontmatter[1].split(/\r?\n/)
80
+ const lineIndex = lines.findIndex((candidate) => /^\s*description\s*:/.test(candidate))
81
+ if (lineIndex < 0) return undefined
82
+
83
+ const raw = lines[lineIndex]!.slice(lines[lineIndex]!.indexOf(':') + 1).trim()
84
+ if (raw === '>-' || raw === '>' || raw === '|-' || raw === '|') {
85
+ const block: string[] = []
86
+ for (const line of lines.slice(lineIndex + 1)) {
87
+ if (/^\s+\S/.test(line)) {
88
+ block.push(line.trim())
89
+ continue
90
+ }
91
+ if (line.trim().length === 0) continue
92
+ break
93
+ }
94
+ const text = block.join(' ').trim()
95
+ return text.length > 0 ? text : undefined
96
+ }
97
+
98
+ const unquoted = raw.replace(/^["']/, '').replace(/["']$/, '')
99
+ return unquoted.length > 0 ? unquoted : undefined
100
+ }
101
+
102
+ /** Read the bundled HQSCH scenario-b SKILL.md. */
103
+ export function readBundledSkill(moduleUrl: string, override?: string): {
104
+ dir: string
105
+ name: string
106
+ description: string
107
+ content: string
108
+ } {
109
+ const dir = requireSkillDir(moduleUrl, override)
110
+ const content = readFileSync(join(dir, 'SKILL.md'), 'utf8')
111
+ return {
112
+ dir,
113
+ name: HQSCH_SKILL_NAME,
114
+ description: skillDescription(content) ?? FALLBACK_SKILL_DESCRIPTION,
115
+ content,
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Read the bundled skill, or `null` when the installed package does not carry
121
+ * one. A missing skill is a packaging failure, but it should degrade this
122
+ * plugin instead of killing the whole DSH process.
123
+ */
124
+ export function tryReadBundledSkill(
125
+ moduleUrl: string,
126
+ override?: string,
127
+ ): { dir: string; name: string; description: string; content: string } | null {
128
+ try {
129
+ return readBundledSkill(moduleUrl, override)
130
+ } catch (err) {
131
+ log.error(
132
+ 'dsh-hqsch: bundled skill unavailable - continuing degraded without skill registration',
133
+ {
134
+ expectedDir: resolveSkillDir(moduleUrl, override),
135
+ error: String((err as Error)?.message ?? err),
136
+ },
137
+ )
138
+ return null
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Host plugin body - register the bundled HQSCH scenario-b skill.
144
+ */
145
+ export function apply(ctx: Context, config: HqschConfigInput = {}): () => void {
146
+ if (!ctx.skills || typeof ctx.skills.register !== 'function') {
147
+ throw new Error('@huaqiu/dsh-hqsch requires the DSH `skills` service (ctx.skills.register).')
148
+ }
149
+
150
+ const skill = tryReadBundledSkill(import.meta.url, config.skillsDir)
151
+
152
+ log.info('applying dsh-hqsch node half', {
153
+ hasConfigHost: hasHostConfig(config),
154
+ skillDir: skill?.dir ?? resolveSkillDir(import.meta.url, config.skillsDir),
155
+ skillPresent: skill !== null,
156
+ })
157
+
158
+ const disposers: Array<() => void> = []
159
+
160
+ if (skill) {
161
+ disposers.push(
162
+ ctx.skills.register({
163
+ name: skill.name,
164
+ description: skill.description,
165
+ content: skill.content,
166
+ source: 'runtime',
167
+ resourceBase: { kind: 'directory', path: skill.dir },
168
+ }),
169
+ )
170
+ }
171
+
172
+ log.info('dsh-hqsch node half ready', {
173
+ skill: skill?.name ?? null,
174
+ degraded: skill === null,
175
+ })
176
+
177
+ return () => {
178
+ for (const dispose of disposers.reverse()) {
179
+ try {
180
+ dispose()
181
+ } catch (err) {
182
+ log.warn('dsh-hqsch disposer failed', { error: String((err as Error)?.message ?? err) })
183
+ }
184
+ }
185
+ }
186
+ }