@windypro-rourou/dsh-eda 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -60,12 +60,13 @@ export const EDA_GUIDANCE =
60
60
  '本机已安装 dsh-lichuang-eda 插件(嘉立创 EDA 助手):本插件是「平台层」——提供官方桥的装/启/连/状态,以及执行官方 eda.* API 的双手;「生成电路图」这类编排请在对话里直接进行(skill 式直觉)。' +
61
61
  '当用户说「嘉立创EDA,启动!」/「帮我画一个…电路」且已连接时,按此流程做:① `eda_status` 确认官方桥 connected(未连则 `eda_backend_connect`);② 用 `eda_exec` 逐步执行官方 eda.* API 在用户的云端画板上实时生成(放元件→连线→网络标签→DRC/网表/BOM),' +
62
62
  '官方案例/参数在 ~/.dsh/eda/bridge/(docs、user-guide、guide、format 目录为官方文档,先用 read 查阅再调用);③ 每步 `eda_exec` 后返回结果/错误,必要时 `eda_sch_drc` 校验。' +
63
- '工具:eda_status(状态)、eda_bridge_install(一键装官方桥)、eda_backend_connect(启动/连接官方桥)、eda_exec(执行任意官方 eda.* 代码——云端实时生成的双手)、eda_pick_spot(**框内定位**:读页面尺寸+已有图元→返回互不冲突的框内网格空位;放元件前先调用)、eda_snapshot(**紧急保存**:画板现场 .epro2 + 预览 SVG + 网表/BOM + agent 动作日志 → ~/.dsh/eda/snapshots/,断连也能留档)、' +
63
+ '工具:eda_status(状态)、eda_bridge_install(一键装官方桥)、eda_backend_connect(启动/连接官方桥)、eda_exec(执行任意官方 eda.* 代码——云端实时生成的双手)、eda_pick_spot(**框内定位**:读页面尺寸+已有图元→返回互不冲突的框内网格空位;放元件前先调用)、eda_place(搜索+定位+放置一体并返回引脚坐标)、eda_wire(引脚级连线,自动校验水平/垂直)、eda_netflag(电源/地网络标志)、eda_save(保存当前文档)——高频画板动作建议用这四个封装而非手拼代码;eda_snapshot(**紧急保存**:画板现场 .epro2 + 预览 SVG + 网表/BOM + agent 动作日志 → ~/.dsh/eda/snapshots/,断连也能留档)、' +
64
64
  'eda_sch_drc / eda_get_netlist / eda_get_bom(校验/导出,需连接)、eda_generate_schematic_json(**离线兜底**:本地生成可导入的标准版 JSON,写入 ~/.dsh/eda/output/;仅在无法连接画板时使用,连接时一律走官方 API 云端实时)、' +
65
65
  'eda_template_list / eda_translate_request(模板与需求翻译,供离线/兜底使用)。' +
66
- '**官方 Skill 全库已接入**:官方易达 skill 知识随插件 vendored(SKILL.md 总纲 / references/classes/*.md 逐类 API / guide、user-guide、format 教程与格式)。规划任何画板动作前先用 eda_skill_read 查阅(顺序:INDEX.md → 对应类文档 → eda_exec 落地);不确定 API 存在性也可先 eda_capabilities。' +
66
+ '**官方 Skill 全库已接入**:官方易达 skill 知识随插件 vendored(SKILL.md 总纲 / references/classes/*.md 逐类 API / guide、user-guide、format 教程与格式;线上镜像 https://prodocs.easyeda.com/cn/api/reference/pro-api.html 可交叉核对)。规划任何画板动作前先用 eda_skill_read 查阅(顺序:INDEX.md → 对应类文档 → eda_exec 落地);不确定 API 存在性也可先 eda_capabilities。' +
67
67
  '**放置定位(硬规则,禁止乱放)**:原理图坐标单位 10mil、图纸 A4≈1170×825,放置前必须先 `eda_exec` 读页面尺寸(dmt_Project.getCurrentProjectInfo()→titleBlockData.Width/Height)与已有图元位置(sch_PrimitiveComponent.getAll()+getState_X/Y 在代码内映射),只放**图框内边距≥80、网格 100 步进、与已有图元间距≥150**的空位(或直接调 eda_pick_spot);导线用 getAllPinsByPrimitiveId 取真实引脚坐标后画水平/垂直线段(只连引脚)。PCB 坐标单位 1mil,同样读已有焊盘(getAllPins)后居中布件、焊盘级走线。' +
68
68
  '**已知缺陷与技巧(实测,勿踩坑)**:`pcb_Document.importChanges` 与 `pcb_Net.setNetlist` 实测不可靠(勿用于网表同步);`sch_Netlist.getNetlist` 与 `pcb_Net.getNetlist("EasyEDA")` 会挂起(网表一律用 `sch_ManufactureData.getNetlistFile` 并 `await f.text()`);`getBomFile` 返回二进制 xlsx(arrayBuffer→base64 处理);DRC 用 `check(true,false,true)` 拿详情;`sch_PrimitiveComponent.getAll` 间歇失败需重试;PCB 板框(layer 11)官方无绘制 API(人工/导入)。' +
69
+ '**超时防线(防自动退出)**:所有聚合/查询工具内部单查询 5s 封顶、并行、单项降级(返回值带 degraded 说明),单次调用总耗时有界——不会触发超时中断;`eda_exec` 可传 `timeoutMs`(默认 25000,定位挂起用 8000~15000,长导出用 45000)。' +
69
70
  '**主动协作(能力入口)**:你能主动为用户完成——读工程/文档、放任意大类元件(搜库→定位→放置)、引脚级连线、网络标志/端口、DRC、网表/BOM 导出、PCB 元件/过孔/走线、现场截图、紧急保存。规划任务时若不确定某个 API 是否存在或怎么调,先调 `eda_capabilities`(结构化能力清单+注意事项+实测片段),再用 `eda_exec` 落地;用户提到画板相关需求时主动提议并执行,不要等用户一字一句教。' +
70
71
  '限制:v0 离线生成器仅支持 电阻/LED;云端实时生成能力由官方 eda.* API 决定(以 ~/.dsh/eda/bridge 内官方文档为准)。' +
71
72
  '用户提到「嘉立创 / 立创EDA / 原理图 / PCB草图 / 生成电路图」时即指本插件,请据此主动协作。'
@@ -88,25 +89,29 @@ function pushPendingActivity(entry) { return activityLog?.push({ status: 'pendin
88
89
  function updateActivity(id, patch) { activityLog?.update(id, patch) }
89
90
  function activityFeed(opts) { return activityLog?.feed(opts) ?? { currentSid: '', sessions: [], activities: [] } }
90
91
 
91
- /** Snapshot the current document's primitive ids (for 撤回 diff). */
92
+ /** Snapshot the current document's primitive ids (for 撤回 diff). 内部每类 5s 封顶。 */
92
93
  async function captureDocumentIds(backend) {
93
94
  const CODE = `return await (async () => {
94
- const info = await eda.dmt_SelectControl.getCurrentDocumentInfo();
95
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
96
+ const T = 5000;
97
+ const pull = (fn) => Promise.race([fn().catch(() => null), sleep(T).then(() => null)]);
98
+ let info = null;
99
+ try { info = await pull(() => eda.dmt_SelectControl.getCurrentDocumentInfo()); } catch (e) {}
95
100
  const domain = info?.documentType === 3 ? 'pcb' : 'sch';
96
101
  const out = { domain };
97
102
  try {
98
103
  if (domain === 'sch') {
99
- out.component = await eda.sch_PrimitiveComponent.getAllPrimitiveId();
100
- out.wire = await eda.sch_PrimitiveWire.getAllPrimitiveId();
104
+ out.component = await pull(() => eda.sch_PrimitiveComponent.getAllPrimitiveId()) ?? [];
105
+ out.wire = await pull(() => eda.sch_PrimitiveWire.getAllPrimitiveId()) ?? [];
101
106
  } else {
102
- out.component = await eda.pcb_PrimitiveComponent.getAllPrimitiveId();
103
- out.via = await eda.pcb_PrimitiveVia.getAllPrimitiveId();
104
- out.line = await eda.pcb_PrimitiveLine.getAllPrimitiveId();
107
+ out.component = await pull(() => eda.pcb_PrimitiveComponent.getAllPrimitiveId()) ?? [];
108
+ out.via = await pull(() => eda.pcb_PrimitiveVia.getAllPrimitiveId()) ?? [];
109
+ out.line = await pull(() => eda.pcb_PrimitiveLine.getAllPrimitiveId()) ?? [];
105
110
  }
106
111
  } catch { /* partial snapshot is fine */ }
107
112
  return out;
108
113
  })();`
109
- const r = await backend.execute(CODE, 25000)
114
+ const r = await backend.execute(CODE, 20000)
110
115
  if (!r.ok) return null
111
116
  try { return JSON.parse(r.result) } catch { return null }
112
117
  }
@@ -556,6 +561,7 @@ function edaGenerateSchematicJsonTool() {
556
561
  ok: { type: 'boolean', required: true },
557
562
  title: { type: 'string' },
558
563
  note: { type: 'string' },
564
+ netSummary: { type: 'string' },
559
565
  components: { type: 'array', items: { type: 'string' } },
560
566
  nets: { type: 'array', items: { type: 'string' } },
561
567
  structureOk: { type: 'boolean' },
@@ -610,7 +616,7 @@ function edaBridgeInstallTool(backend) {
610
616
  error: { type: 'string' },
611
617
  },
612
618
  },
613
- render: (_args, value) => ({ type: 'text', text: value?.ok ? (value.message ?? '安装完成') : `安装失败: ${value?.error ?? '?'}` }),
619
+ render: (_args, value) => [{ type: 'text', text: value?.ok ? (value.message ?? '安装完成') : `安装失败: ${value?.error ?? '?'}` }],
614
620
  },
615
621
  async execute(args, exec) {
616
622
  const sid = sessionIdOf(exec)
@@ -652,12 +658,12 @@ function edaBackendConnectTool(handle, backend) {
652
658
  note: { type: 'string' },
653
659
  },
654
660
  },
655
- render: (_args, value) => ({
661
+ render: (_args, value) => [{
656
662
  type: 'text',
657
663
  text: value?.ok === false
658
664
  ? `启动失败: ${value?.error ?? '?'}\n${value?.note ?? ''}`
659
665
  : `官方桥: ${value?.state ?? '?'}\n${value?.note ?? ''}`,
660
- }),
666
+ }],
661
667
  },
662
668
  async execute(_args, exec) {
663
669
  const sid = sessionIdOf(exec)
@@ -695,7 +701,7 @@ function bridgeTool(handle, backend, spec) {
695
701
  error: { type: 'string' },
696
702
  },
697
703
  },
698
- render: (_args, value) => ({ type: 'text', text: value?.ok ? renderSummary(value.result) : `失败: ${value?.error ?? '?'}` }),
704
+ render: (_args, value) => [{ type: 'text', text: value?.ok ? renderSummary(value.result) : `失败: ${value?.error ?? '?'}` }],
699
705
  },
700
706
  async execute(args, exec) {
701
707
  const sid = sessionIdOf(exec)
@@ -768,6 +774,7 @@ function edaExecTool(backend) {
768
774
  parameters: {
769
775
  code: { type: 'string', description: '官方 eda.* JS 代码,例如 return await eda.dmt_Project.getCurrentProjectInfo();' },
770
776
  args: { type: 'string', description: '可选:附加 JSON(未来扩展位;可忽略)' },
777
+ timeoutMs: { type: 'integer', description: '可选:本调用的客户端超时毫秒(默认 25000;范围 5000-120000)。定位挂起问题用 8000~15000 快速失败;保存/导出等长操作可给 45000' },
771
778
  },
772
779
  output: {
773
780
  schema: {
@@ -779,12 +786,13 @@ function edaExecTool(backend) {
779
786
  error: { type: 'string' },
780
787
  },
781
788
  },
782
- render: (_args, value) => ({ type: 'text', text: value?.ok ? `执行结果:\n${value.result ?? ''}` : `执行失败: ${value?.error ?? '?'}` }),
789
+ render: (_args, value) => [{ type: 'text', text: value?.ok ? `执行结果:\n${value.result ?? ''}` : `执行失败: ${value?.error ?? '?'}` }],
783
790
  },
784
791
  async execute(args, exec) {
785
792
  const sid = sessionIdOf(exec)
786
793
  const code = typeof args?.code === 'string' ? args.code.trim() : ''
787
794
  if (code === '') return { ok: false, error: 'code 不能为空(例如 return await eda.dmt_Project.getCurrentProjectInfo();)' }
795
+ const tms = Math.max(5000, Math.min(120000, Number(args?.timeoutMs) || 25000))
788
796
  await backend.refresh() // live facts before gating
789
797
  const st = statusOf(backend)
790
798
  if (!st.connected) {
@@ -793,9 +801,9 @@ function edaExecTool(backend) {
793
801
  }
794
802
  const id = pushPendingActivity({ sid, tool: 'eda_exec', action: describeEdaAction(code), code })
795
803
  const t0 = Date.now()
796
- const before = await captureDocumentIds(backend) // best-effort (for 撤回)
797
- const r = await backend.execute(code)
798
- const after = await captureDocumentIds(backend)
804
+ const before = await captureDocumentIds(backend) // best-effort (for 撤回); 自身 5s 封顶
805
+ const r = await backend.execute(code, tms)
806
+ const after = await captureDocumentIds(backend) // 5s 封顶,绝不追加挂起
799
807
  updateActivity(id, {
800
808
  status: r.ok ? 'done' : 'error',
801
809
  ok: r.ok,
@@ -810,23 +818,22 @@ function edaExecTool(backend) {
810
818
  })
811
819
  }
812
820
 
813
- /** 读取当前原理图页的尺寸 + 已有图元分布(桥端映射;getAll 间歇失败→重试)。 */
821
+ /** 读取当前原理图页的尺寸 + 已有图元分布(桥端映射;每项 5s 封顶,绝不挂满)。 */
814
822
  async function readSchLayout(backend) {
815
823
  const CODE = `return await (async () => {
816
- const proj = await eda.dmt_Project.getCurrentProjectInfo();
824
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
825
+ const T = 5000;
826
+ const pull = (fn) => Promise.race([fn().catch(() => null), sleep(T).then(() => null)]);
827
+ const proj = await pull(() => eda.dmt_Project.getCurrentProjectInfo());
817
828
  const page = proj?.data?.[0]?.schematic?.page?.[0];
818
- let comps = null; let wires = null;
819
- for (let i = 0; i < 3 && (comps == null || wires == null); i++) {
820
- try { comps = await eda.sch_PrimitiveComponent.getAll(); } catch (e) { comps = null; }
821
- try { wires = await eda.sch_PrimitiveWire.getAll(); } catch (e) { wires = null; }
822
- if (comps == null || wires == null) await new Promise(r => setTimeout(r, 700));
823
- }
829
+ const comps = (await pull(() => eda.sch_PrimitiveComponent.getAll())) || [];
830
+ const wires = (await pull(() => eda.sch_PrimitiveWire.getAll())) || [];
824
831
  const used = [];
825
- for (const c of (comps || [])) { try { used.push({ x: c.getState_X(), y: c.getState_Y() }); } catch (e) {} }
826
- for (const w of (wires || [])) { try { const l = w.getState_Line(); for (let i = 0; i + 1 < l.length; i += 2) used.push({ x: l[i], y: l[i + 1] }); } catch (e) {} }
832
+ for (const c of comps) { try { used.push({ x: c.getState_X(), y: c.getState_Y() }); } catch (e) {} }
833
+ for (const w of wires) { try { const l = w.getState_Line(); for (let i = 0; i + 1 < l.length; i += 2) used.push({ x: l[i], y: l[i + 1] }); } catch (e) {} }
827
834
  return { name: page?.name ?? '', width: Number(page?.titleBlockData?.Width?.value ?? 1170), height: Number(page?.titleBlockData?.Height?.value ?? 825), used };
828
835
  })();`
829
- const r = await backend.execute(CODE, 30000)
836
+ const r = await backend.execute(CODE, 18000)
830
837
  if (!r.ok) return { ok: false, error: r.error }
831
838
  try { return { ok: true, ...JSON.parse(r.result) } } catch { return { ok: false, error: 'layout read failed' } }
832
839
  }
@@ -870,13 +877,13 @@ function edaPickSpotTool(backend) {
870
877
  },
871
878
  },
872
879
  render: (_args, value) => {
873
- if (value?.ok !== true) return { type: 'text', text: `框内定位失败: ${value?.error ?? '?'}` }
880
+ if (value?.ok !== true) return [{ type: 'text', text: `框内定位失败: ${value?.error ?? '?'}` }]
874
881
  const lines = [
875
882
  `页面: ${value.page ?? '?'}(${value.size ?? '?'})· 已有图元采样 ${value.usedCount ?? 0}`,
876
883
  ]
877
884
  for (const s of value.spots ?? []) lines.push(` 空位: (${s.x}, ${s.y})${s.inside ? '' : ' ⚠框外'}`)
878
885
  if (value.note) lines.push(`提示: ${value.note}`)
879
- return { type: 'text', text: lines.join('\n') }
886
+ return [{ type: 'text', text: lines.join('\n') }]
880
887
  },
881
888
  },
882
889
  async execute(args) {
@@ -912,19 +919,23 @@ function edaPickSpotTool(backend) {
912
919
  })
913
920
  }
914
921
 
915
- /** 读 PCB 现有图元分布(元件/过孔/线),供 pcb 定位。 */
922
+ /** 读 PCB 现有图元分布(元件/过孔/线),供 pcb 定位。每类 5s 封顶 + 并行。 */
916
923
  async function readPcbLayout(backend) {
917
924
  const CODE = `return await (async () => {
918
- const used = [];
925
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
926
+ const T = 5000;
927
+ const pull = (fn) => Promise.race([fn().catch(() => null), sleep(T).then(() => null)]);
919
928
  const grab = async (api) => {
920
- try { const all = await eda[api].getAll(); for (const x of (all || [])) { try { used.push({ x: x.getState_X(), y: x.getState_Y() }); } catch (e) {} } } catch (e) {}
929
+ const all = (await pull(() => eda[api].getAll())) || [];
930
+ const used = [];
931
+ for (const x of all) { try { used.push({ x: x.getState_X(), y: x.getState_Y() }); } catch (e) {} }
932
+ return used;
921
933
  };
922
- await grab('pcb_PrimitiveComponent');
923
- await grab('pcb_PrimitiveVia');
924
- await grab('pcb_PrimitiveLine');
934
+ const [a, b, c] = await Promise.all([grab('pcb_PrimitiveComponent'), grab('pcb_PrimitiveVia'), grab('pcb_PrimitiveLine')]);
935
+ const used = [...a, ...b, ...c];
925
936
  return { used, count: used.length };
926
937
  })();`
927
- const r = await backend.execute(CODE, 30000)
938
+ const r = await backend.execute(CODE, 15000)
928
939
  if (!r.ok) return { ok: false, error: r.error }
929
940
  try { const j = JSON.parse(r.result); return { ok: true, used: j.used ?? [], count: j.count ?? 0 } } catch { return { ok: false, error: 'pcb layout read failed' } }
930
941
  }
@@ -969,11 +980,12 @@ function edaBoardOverviewTool(backend) {
969
980
  },
970
981
  },
971
982
  truncated: { type: 'boolean' },
983
+ degraded: { type: 'array', items: { type: 'string' } },
972
984
  error: { type: 'string' },
973
985
  },
974
986
  },
975
987
  render: (_args, value) => {
976
- if (value?.ok !== true) return { type: 'text', text: `画板全览失败: ${value?.error ?? '?'}` }
988
+ if (value?.ok !== true) return [{ type: 'text', text: `画板全览失败: ${value?.error ?? '?'}` }]
977
989
  const lines = [
978
990
  `${value.domain === 'pcb' ? 'PCB' : '原理图'}(${value.doc ?? '?'})· ${value.pageSize ?? ''}`,
979
991
  `元件 ${value.componentCount ?? 0} · 导线 ${value.wireCount ?? 0} · 过孔 ${value.viaCount ?? 0} · 走线 ${value.lineCount ?? 0}`,
@@ -984,43 +996,50 @@ function edaBoardOverviewTool(backend) {
984
996
  for (const c of value.componentsPreview.slice(0, 20)) lines.push(` ${c.designator} ${c.name} @(${c.x},${c.y})${c.net ? ` [${c.net}]` : ''}`)
985
997
  if (value.truncated) lines.push(` …共 ${value.componentCount} 个(full=true 看全)`)
986
998
  }
987
- return { type: 'text', text: lines.join('\n') }
999
+ if ((value.degraded ?? []).length > 0) lines.push(`⚠ 单项降级: ${value.degraded.join('; ')}(可用最小粒度 eda_exec 单查该项)`)
1000
+ return [{ type: 'text', text: lines.join('\n') }]
988
1001
  },
989
1002
  },
990
1003
  async execute(args) {
991
1004
  const full = args?.full === true
992
- const r = await backend.execute(OVERVIEW_CODE, 40000)
993
- if (!r.ok) return { ok: false, error: r.error, domain: 'sch', doc: '', pageSize: '', componentCount: 0, wireCount: 0, viaCount: 0, lineCount: 0, nets: [], componentsPreview: [], truncated: false }
1005
+ const r = await backend.execute(OVERVIEW_CODE, 30000) // 内部单查询 5s 封顶+并行 → 总耗时 <15s,不触发超时中断
1006
+ if (!r.ok) return { ok: false, error: r.error, domain: 'sch', doc: '', pageSize: '', componentCount: 0, wireCount: 0, viaCount: 0, lineCount: 0, nets: [], componentsPreview: [], truncated: false, degraded: [] }
994
1007
  let raw
995
- try { raw = JSON.parse(r.result) } catch { return { ok: false, error: 'overview parse failed', domain: 'sch', doc: '', pageSize: '', componentCount: 0, wireCount: 0, viaCount: 0, lineCount: 0, nets: [], componentsPreview: [], truncated: false } }
1008
+ try { raw = JSON.parse(r.result) } catch { return { ok: false, error: 'overview parse failed', domain: 'sch', doc: '', pageSize: '', componentCount: 0, wireCount: 0, viaCount: 0, lineCount: 0, nets: [], componentsPreview: [], truncated: false, degraded: [] } }
996
1009
  const o = buildOverview(raw, { full })
997
- return { ok: true, ...o, doc: raw?.doc?.name ?? raw?.doc?.uuid ?? '' }
1010
+ return { ok: true, ...o, doc: raw?.doc?.name ?? raw?.doc?.uuid ?? '', degraded: Array.isArray(raw?.degraded) ? raw.degraded : [] }
998
1011
  },
999
1012
  })
1000
1013
  }
1001
1014
 
1002
1015
  const OVERVIEW_CODE = `return await (async () => {
1003
1016
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
1004
- let info = null;
1005
- for (let i = 0; i < 3 && info == null; i++) {
1006
- try { info = await eda.dmt_SelectControl.getCurrentDocumentInfo(); } catch (e) { info = null; }
1007
- if (info == null) await sleep(700);
1008
- }
1009
- const domain = info?.documentType === 3 ? 'pcb' : 'sch';
1010
- const out = { domain, doc: info };
1017
+ const TIMEOUT = 5000; // 每个子查询 5s 封顶:单点挂起只降级该项,绝不拖垮整个调用
1018
+ const pull = (fn, label) => Promise.race([
1019
+ (async () => { try { return { ok: true, value: await fn() }; } catch (e) { return { ok: false, label, err: String(e).slice(0, 80) }; } })(),
1020
+ sleep(TIMEOUT).then(() => ({ ok: false, label, err: 'timeout' })),
1021
+ ]);
1022
+ const info = await pull(() => eda.dmt_SelectControl.getCurrentDocumentInfo(), 'doc info');
1023
+ const domain = (info.ok && info.value?.documentType === 3) ? 'pcb' : 'sch';
1024
+ const out = { domain, doc: info.ok ? info.value : null, degraded: [] };
1025
+ if (!info.ok) out.degraded.push(info.label + ': ' + info.err);
1011
1026
  if (domain === 'sch') {
1012
- try {
1013
- const proj = await eda.dmt_Project.getCurrentProjectInfo();
1014
- const page = proj?.data?.[0]?.schematic?.page?.[0];
1027
+ const proj = await pull(() => eda.dmt_Project.getCurrentProjectInfo(), 'project info');
1028
+ if (proj.ok && proj.value?.data?.[0]?.schematic?.page?.[0]) {
1029
+ const page = proj.value.data[0].schematic.page[0];
1015
1030
  out.pageSize = { w: Number(page?.titleBlockData?.Width?.value ?? 1170), h: Number(page?.titleBlockData?.Height?.value ?? 825), unit: '10mil' };
1016
- } catch (e) {}
1017
- let comps = []; let wires = [];
1018
- for (let i = 0; i < 3; i++) {
1019
- try { comps = await eda.sch_PrimitiveComponent.getAll(); break; } catch (e) { await sleep(700); }
1020
- }
1021
- try { wires = await eda.sch_PrimitiveWire.getAll(); } catch (e) { wires = []; }
1031
+ } else out.degraded.push(proj.label)
1032
+ // 并行取元件与导线(各自 5s 封顶)
1033
+ const [compsR, wiresR] = await Promise.all([
1034
+ pull(() => eda.sch_PrimitiveComponent.getAll(), 'components'),
1035
+ pull(() => eda.sch_PrimitiveWire.getAll(), 'wires'),
1036
+ ]);
1037
+ if (!compsR.ok) out.degraded.push(compsR.label + ': ' + compsR.err)
1038
+ if (!wiresR.ok) out.degraded.push(wiresR.label + ': ' + wiresR.err)
1039
+ const comps = compsR.ok ? (compsR.value || []) : [];
1040
+ const wires = wiresR.ok ? (wiresR.value || []) : [];
1022
1041
  const nets = new Set();
1023
- out.components = (comps || []).map(c => {
1042
+ out.components = comps.map(c => {
1024
1043
  let d = ''; let t = ''; let n = ''; let x = 0; let y = 0;
1025
1044
  try { d = (c.getState_Designator ? c.getState_Designator() : '') || ''; } catch (e) {}
1026
1045
  try { t = (c.getState_ComponentType ? c.getState_ComponentType() : '') || ''; } catch (e) {}
@@ -1028,22 +1047,22 @@ const OVERVIEW_CODE = `return await (async () => {
1028
1047
  try { x = c.getState_X(); y = c.getState_Y(); } catch (e) {}
1029
1048
  return { id: c.getState_PrimitiveId ? c.getState_PrimitiveId() : '', d, n, t, x, y };
1030
1049
  });
1031
- out.wireCount = (wires || []).length;
1032
- try { for (const w of (wires || [])) { const n = w.getState_Net ? w.getState_Net() : ''; if (n) nets.add(n); } } catch (e) {}
1050
+ out.wireCount = wires.length;
1051
+ try { for (const w of wires) { const n = w.getState_Net ? w.getState_Net() : ''; if (n) nets.add(n); } } catch (e) {}
1033
1052
  out.nets = [...nets];
1034
1053
  } else {
1035
1054
  const grab = async (api) => {
1036
- try { const all = await eda[api].getAll(); return (all || []).map(x => {
1055
+ const r = await pull(() => eda[api].getAll(), api);
1056
+ if (!r.ok) { out.degraded.push(r.label + ': ' + r.err); return []; }
1057
+ return (r.value || []).map(x => {
1037
1058
  let d = ''; let n = ''; let x = 0; let y = 0;
1038
1059
  try { d = (x.getState_Designator ? x.getState_Designator() : '') || ''; } catch (e) {}
1039
1060
  try { n = (x.getState_Net ? x.getState_Net() : '') || ''; } catch (e) {}
1040
1061
  try { x = x.getState_X(); y = x.getState_Y(); } catch (e) {}
1041
1062
  return { id: x.getState_PrimitiveId ? x.getState_PrimitiveId() : '', d, n, t: 'part', x, y };
1042
- }); } catch (e) { return []; }
1063
+ });
1043
1064
  };
1044
- const comps = await grab('pcb_PrimitiveComponent');
1045
- const vias = await grab('pcb_PrimitiveVia');
1046
- const lines = await grab('pcb_PrimitiveLine');
1065
+ const [comps, vias, lines] = await Promise.all([grab('pcb_PrimitiveComponent'), grab('pcb_PrimitiveVia'), grab('pcb_PrimitiveLine')]);
1047
1066
  out.components = comps; out.viaCount = vias.length; out.lineCount = lines.length; out.wireCount = 0;
1048
1067
  const nets = new Set();
1049
1068
  for (const x of [...comps, ...vias, ...lines]) if (x.n) nets.add(x.n);
@@ -1074,7 +1093,7 @@ function edaTraceTool(backend) {
1074
1093
  error: { type: 'string' },
1075
1094
  },
1076
1095
  },
1077
- render: (_args, value) => ({ type: 'text', text: value?.ok ? `✅ 截图已保存: ${value.path}(${((value.size ?? 0) / 1024).toFixed(1)}KB)` : `截图失败: ${value?.error ?? '?'}` }),
1096
+ render: (_args, value) => [{ type: 'text', text: value?.ok ? `✅ 截图已保存: ${value.path}(${((value.size ?? 0) / 1024).toFixed(1)}KB)` : `截图失败: ${value?.error ?? '?'}` }],
1078
1097
  },
1079
1098
  async execute(args) {
1080
1099
  const r = await backend.execute(TRACE_CODE, 60000)
@@ -1120,7 +1139,7 @@ function edaVerifyTool(handle, backend) {
1120
1139
  note: { type: 'string' },
1121
1140
  },
1122
1141
  },
1123
- render: (_args, value) => ({
1142
+ render: (_args, value) => [{
1124
1143
  type: 'text',
1125
1144
  text: [
1126
1145
  `验收: ${value?.ok ? '完成' : '部分失败'}`,
@@ -1130,7 +1149,7 @@ function edaVerifyTool(handle, backend) {
1130
1149
  ...(value?.errors ?? []).map((e) => `⚠ ${e}`),
1131
1150
  value?.note ?? '',
1132
1151
  ].filter(Boolean).join('\n'),
1133
- }),
1152
+ }],
1134
1153
  },
1135
1154
  async execute(_args, exec) {
1136
1155
  const sid = sessionIdOf(exec)
@@ -1289,7 +1308,7 @@ function edaSkillReadTool() {
1289
1308
  },
1290
1309
  },
1291
1310
  render: (_args, value) => {
1292
- if (value?.ok !== true) return { type: 'text', text: `紧急保存失败: ${value?.error ?? '?'}` }
1311
+ if (value?.ok !== true) return [{ type: 'text', text: `紧急保存失败: ${value?.error ?? '?'}` }]
1293
1312
  const lines = [
1294
1313
  `✅ 已紧急保存 → ${value.dir ?? ''}`,
1295
1314
  `文件: ${(value?.files ?? []).map((f) => f.name).join(', ') || '无'}`,
@@ -1297,7 +1316,7 @@ function edaSkillReadTool() {
1297
1316
  value?.preview ? `通用预览: ${value.preview}` : '',
1298
1317
  ].filter((s) => s !== '')
1299
1318
  if ((value?.errors ?? []).length > 0) lines.push(`降级项: ${value.errors.join('; ')}`)
1300
- return { type: 'text', text: lines.join('\n') }
1319
+ return [{ type: 'text', text: lines.join('\n') }]
1301
1320
  },
1302
1321
  },
1303
1322
  async execute(_args, exec) {
@@ -1324,6 +1343,387 @@ function edaSkillReadTool() {
1324
1343
  })
1325
1344
  }
1326
1345
 
1346
+ /** ---------------------------------------------------- deterministic wrappers */
1347
+
1348
+ /** 系统库搜索 + 放置(sch):搜索→create→读位号/引脚;pins 供画线用。 */
1349
+ function schPlaceCode(keyword, x, y) {
1350
+ return `return await (async () => {
1351
+ const devices = await eda.lib_Device.search(${JSON.stringify(keyword)});
1352
+ if (!devices || devices.length === 0) return { ok: false, error: '未搜索到器件' };
1353
+ const dev = devices[0];
1354
+ const comp = await eda.sch_PrimitiveComponent.create(dev, ${x}, ${y}, undefined, 0, false, true, true);
1355
+ const id = comp.getState_PrimitiveId();
1356
+ let ref = ''; try { ref = comp.getState_Designator() || ''; } catch (e) {}
1357
+ let name = ''; try { name = dev.name || ''; } catch (e) { name = dev.name || ''; }
1358
+ let pins = [];
1359
+ try { const ps = await eda.sch_PrimitiveComponent.getAllPinsByPrimitiveId(id); for (const p of (ps || [])) pins.push({ x: p.getState_X(), y: p.getState_Y() }); } catch (e) {}
1360
+ return { ok: true, ref, id, pins, x: ${x}, y: ${y}, name, note: 'sch 原理图元件' };
1361
+ })();`
1362
+ }
1363
+
1364
+ function pcbPlaceCode(keyword, x, y) {
1365
+ return `return await (async () => {
1366
+ const devices = await eda.lib_Device.search(${JSON.stringify(keyword)});
1367
+ if (!devices || devices.length === 0) return { ok: false, error: '未搜索到器件' };
1368
+ const dev = devices[0];
1369
+ const comp = await eda.pcb_PrimitiveComponent.create(dev, 1, ${x}, ${y}, 0, false);
1370
+ const id = comp.getState_PrimitiveId();
1371
+ let ref = ''; try { ref = comp.getState_Designator() || ''; } catch (e) {}
1372
+ let name = ''; try { name = dev.name || ''; } catch (e) { name = dev.name || ''; }
1373
+ let pins = [];
1374
+ try { const rf = await eda.pcb_PrimitiveComponent.get(id); if (rf) { const ps = await rf.getAllPins(); for (const p of (ps || [])) pins.push({ x: p.getState_X(), y: p.getState_Y() }); } } catch (e) {}
1375
+ return { ok: true, ref, id, pins, x: ${x}, y: ${y}, name, note: 'pcb 元件' };
1376
+ })();`
1377
+ }
1378
+
1379
+ /** 引脚级连线(sch):sch_PrimitiveWire.create(segments, net, …)。 */
1380
+ function schWireCode(points, net) {
1381
+ const pts = JSON.stringify(points)
1382
+ const netStr = (typeof net === 'string' && net !== '') ? JSON.stringify(net) : 'null'
1383
+ return `return await (async () => {
1384
+ const wire = await eda.sch_PrimitiveWire.create(${pts}, ${netStr}, null, null, null);
1385
+ let id = ''; try { id = (wire && wire.getState_PrimitiveId) ? wire.getState_PrimitiveId() : ''; } catch (e) {}
1386
+ return { ok: true, primitives: id ? [id] : [], note: 'sch 导线' };
1387
+ })();`
1388
+ }
1389
+
1390
+ /** 引脚级连线(pcb):每对相邻点一条 pcb_PrimitiveLine.create(TOP 层 / 宽 10)。 */
1391
+ function pcbWireCode(points, net) {
1392
+ const pts = JSON.stringify(points)
1393
+ const netStr = JSON.stringify((typeof net === 'string' && net !== '') ? net : '')
1394
+ return `return await (async () => {
1395
+ const pts = ${pts};
1396
+ const net = ${netStr};
1397
+ const segs = [];
1398
+ for (let i = 0; i + 3 < pts.length; i += 2) {
1399
+ const line = await eda.pcb_PrimitiveLine.create(net, 1, pts[i], pts[i + 1], pts[i + 2], pts[i + 3], 10);
1400
+ let id = ''; try { id = (line && line.getState_PrimitiveId) ? line.getState_PrimitiveId() : ''; } catch (e) {}
1401
+ if (id) segs.push(id);
1402
+ }
1403
+ return { ok: true, primitives: segs, note: 'pcb 走线(TOP 层 / 宽 10)' };
1404
+ })();`
1405
+ }
1406
+
1407
+ function schNetflagCode(type, net, x, y) {
1408
+ return `return await (async () => {
1409
+ const flag = await eda.sch_PrimitiveComponent.createNetFlag(${JSON.stringify(type)}, ${JSON.stringify(net)}, ${x}, ${y}, 0, false);
1410
+ if (!flag) return { ok: false, error: 'createNetFlag 未返回图元' };
1411
+ let id = ''; try { id = flag.getState_PrimitiveId ? flag.getState_PrimitiveId() : ''; } catch (e) {}
1412
+ return { ok: true, id, net: ${JSON.stringify(net)}, x: ${x}, y: ${y} };
1413
+ })();`
1414
+ }
1415
+
1416
+ function saveDocCode() {
1417
+ return `return await (async () => {
1418
+ const info = await eda.dmt_SelectControl.getCurrentDocumentInfo();
1419
+ const isPcb = info?.documentType === 3;
1420
+ const saved = isPcb ? await eda.pcb_Document.save() : await eda.sch_Document.save();
1421
+ return { ok: true, saved: saved === true, docType: info?.documentType ?? (isPcb ? 3 : 1) };
1422
+ })();`
1423
+ }
1424
+
1425
+ /** eda_place:搜索器件→框内定位→放置一体,并返回引脚坐标(连线用)。 */
1426
+ function edaPlaceTool(backend) {
1427
+ return defineTool({
1428
+ name: 'eda_place',
1429
+ description: 'Search the system library (keyword), pick a collision-free spot, then place the component in ONE step and return its pins (for wiring). ' +
1430
+ 'kind=sch (default, units 10mil) | pcb (units 1mil). value is optional metadata (default \'?\'). ' +
1431
+ 'Triggers: 放一个元件, place a part, 搜索并放置, put LED on the schematic.',
1432
+ parameters: {
1433
+ keyword: { type: 'string', required: true, description: '系统库搜索关键词,如 LED / R0402 / 10K / ESP32' },
1434
+ kind: { type: 'string', description: 'sch(默认,原理图)| pcb' },
1435
+ value: { type: 'string', description: '可选:值(默认 \'?\')' },
1436
+ },
1437
+ output: {
1438
+ schema: {
1439
+ type: 'object',
1440
+ additionalProperties: false,
1441
+ properties: {
1442
+ ok: { type: 'boolean', required: true },
1443
+ ref: { type: 'string' },
1444
+ id: { type: 'string' },
1445
+ pins: {
1446
+ type: 'array',
1447
+ items: {
1448
+ type: 'object',
1449
+ additionalProperties: false,
1450
+ properties: {
1451
+ x: { type: 'number', required: true },
1452
+ y: { type: 'number', required: true },
1453
+ },
1454
+ },
1455
+ },
1456
+ x: { type: 'number' },
1457
+ y: { type: 'number' },
1458
+ name: { type: 'string' },
1459
+ note: { type: 'string' },
1460
+ error: { type: 'string' },
1461
+ },
1462
+ },
1463
+ render: (_args, value) => {
1464
+ if (value?.ok !== true) return [{ type: 'text', text: `放置失败: ${value?.error ?? '?'}` }]
1465
+ const lines = [
1466
+ `✅ 已放置 ${value.name || '元件'}(${value.ref || '?'})@(${value.x}, ${value.y})`,
1467
+ `primitiveId: ${value.id}`,
1468
+ `引脚(${(value.pins ?? []).length}): ${(value.pins ?? []).map((p) => `(${p.x},${p.y})`).join(' ') || '无'}`,
1469
+ ]
1470
+ if (value.note) lines.push(`note: ${value.note}`)
1471
+ return [{ type: 'text', text: lines.join('\n') }]
1472
+ },
1473
+ },
1474
+ async execute(args, exec) {
1475
+ const sid = sessionIdOf(exec)
1476
+ const keyword = typeof args?.keyword === 'string' ? args.keyword.trim() : ''
1477
+ if (keyword === '') {
1478
+ pushActivity({ sid, tool: 'eda_place', action: '放置元件', ok: false, error: 'keyword 不能为空' })
1479
+ return { ok: false, error: 'keyword 不能为空(系统库搜索关键词)', ref: '', id: '', pins: [], x: 0, y: 0, name: '', note: '' }
1480
+ }
1481
+ const kind = args?.kind === 'pcb' ? 'pcb' : 'sch'
1482
+ const value = typeof args?.value === 'string' && args.value !== '' ? args.value : '?'
1483
+ await backend.refresh() // live facts before gating
1484
+ if (!statusOf(backend).connected) {
1485
+ pushActivity({ sid, tool: 'eda_place', action: '放置元件', ok: false, error: '官方桥未连接(门控拦截)' })
1486
+ return { ok: false, error: '官方桥未连接:请先 eda_backend_connect(并确认扩展 Run API Gateway 已装且开启「外部交互」)。', ref: '', id: '', pins: [], x: 0, y: 0, name: '', note: '' }
1487
+ }
1488
+ // 选点:显式坐标优先;否则读版图挑空位(最多 3 个候选重试)。
1489
+ let candidates = []
1490
+ if (Number.isFinite(args?.x) && Number.isFinite(args?.y)) {
1491
+ candidates = [{ x: Number(args.x), y: Number(args.y) }]
1492
+ } else if (kind === 'sch') {
1493
+ const layout = await readSchLayout(backend)
1494
+ if (!layout.ok) return { ok: false, error: layout.error, ref: '', id: '', pins: [], x: 0, y: 0, name: '', note: '' }
1495
+ candidates = pickSpots({ pageWidth: layout.width, pageHeight: layout.height, used: layout.used, count: 3 })
1496
+ } else {
1497
+ const layout = await readPcbLayout(backend)
1498
+ if (!layout.ok) return { ok: false, error: layout.error, ref: '', id: '', pins: [], x: 0, y: 0, name: '', note: '' }
1499
+ candidates = pickSpotsPcb(layout.used, 3)
1500
+ }
1501
+ if (candidates.length === 0) {
1502
+ return { ok: false, error: '未找到可放置的空位(可先 eda_pick_spot 或检查页面尺寸)', ref: '', id: '', pins: [], x: 0, y: 0, name: '', note: '' }
1503
+ }
1504
+ const actId = pushPendingActivity({ sid, tool: 'eda_place', action: '放置元件' })
1505
+ const t0 = Date.now()
1506
+ let lastError = ''
1507
+ for (const cand of candidates.slice(0, 3)) {
1508
+ const code = kind === 'pcb' ? pcbPlaceCode(keyword, cand.x, cand.y) : schPlaceCode(keyword, cand.x, cand.y)
1509
+ const r = await backend.execute(code, 30000)
1510
+ if (r.ok) {
1511
+ let j = null
1512
+ try { j = JSON.parse(r.result) } catch { j = null }
1513
+ if (j?.ok === true) {
1514
+ updateActivity(actId, { status: 'done', ok: true, action: '放置元件', result: `${j.name || keyword}(${j.ref || '?'})@(${cand.x},${cand.y})·${(j.pins ?? []).length} 引脚`, durationMs: Date.now() - t0 })
1515
+ return {
1516
+ ok: true, ref: j.ref ?? '', id: j.id ?? '', pins: j.pins ?? [],
1517
+ x: Number(j.x ?? cand.x), y: Number(j.y ?? cand.y), name: j.name ?? '',
1518
+ note: j.note ?? (value === '?' ? '' : `值: ${value}`),
1519
+ }
1520
+ }
1521
+ lastError = j?.error || r.error || '放置失败'
1522
+ } else {
1523
+ lastError = r.error
1524
+ }
1525
+ }
1526
+ updateActivity(actId, { status: 'error', ok: false, action: '放置元件', error: lastError, durationMs: Date.now() - t0 })
1527
+ return { ok: false, error: lastError || '放置失败', ref: '', id: '', pins: [], x: candidates[0]?.x ?? 0, y: candidates[0]?.y ?? 0, name: '', note: '' }
1528
+ },
1529
+ })
1530
+ }
1531
+
1532
+ /** eda_wire:引脚/点连线(平铺坐标;服务端校验水平/垂直)。 */
1533
+ function edaWireTool(backend) {
1534
+ return defineTool({
1535
+ name: 'eda_wire',
1536
+ description: 'Draw wires between given points: flat coordinates [x1,y1,x2,y2,...] (each segment must be horizontal or vertical — validated server-side). ' +
1537
+ 'kind=sch (default) | pcb (TOP layer, width 10). net is an optional network name. ' +
1538
+ 'Triggers: 画导线, connect pins, 连线, draw wire, 引脚级连线.',
1539
+ parameters: {
1540
+ points: { type: 'array', items: { type: 'number' }, description: '平铺坐标 [x1,y1,x2,y2,...](必须水平或垂直段)' },
1541
+ net: { type: 'string', description: '可选:网络名' },
1542
+ kind: { type: 'string', description: 'sch(默认)| pcb(固用 TOP 层、线宽 10)' },
1543
+ },
1544
+ output: {
1545
+ schema: {
1546
+ type: 'object',
1547
+ additionalProperties: false,
1548
+ properties: {
1549
+ ok: { type: 'boolean', required: true },
1550
+ primitives: { type: 'array', items: { type: 'string' } },
1551
+ note: { type: 'string' },
1552
+ error: { type: 'string' },
1553
+ },
1554
+ },
1555
+ render: (_args, value) => {
1556
+ if (value?.ok !== true) return [{ type: 'text', text: `画导线失败: ${value?.error ?? '?'}` }]
1557
+ return [{ type: 'text', text: `✅ 已画 ${(value.primitives ?? []).length} 段导线\nids: ${(value.primitives ?? []).join(', ') || '无'}${value.note ? '\n' + value.note : ''}` }]
1558
+ },
1559
+ },
1560
+ async execute(args, exec) {
1561
+ const sid = sessionIdOf(exec)
1562
+ // 先校验参数(再连桥),保证参数错误与桥无关。
1563
+ const pts = Array.isArray(args?.points) ? args.points : null
1564
+ if (!pts || pts.length < 4) {
1565
+ pushActivity({ sid, tool: 'eda_wire', action: '画导线', ok: false, error: '至少两个点' })
1566
+ return { ok: false, error: '至少两个点(平铺坐标 [x1,y1,x2,y2,...])', primitives: [], note: '' }
1567
+ }
1568
+ if (pts.length % 2 !== 0) {
1569
+ return { ok: false, error: '坐标必须成对(每个点 x,y 两个数)', primitives: [], note: '' }
1570
+ }
1571
+ for (let i = 0; i + 3 < pts.length; i += 2) {
1572
+ const x1 = Number(pts[i]); const y1 = Number(pts[i + 1]); const x2 = Number(pts[i + 2]); const y2 = Number(pts[i + 3])
1573
+ if (!Number.isFinite(x1) || !Number.isFinite(y1) || !Number.isFinite(x2) || !Number.isFinite(y2)) {
1574
+ return { ok: false, error: `第 ${i / 2 + 1} 段坐标非法`, primitives: [], note: '' }
1575
+ }
1576
+ if (x1 !== x2 && y1 !== y2) {
1577
+ return { ok: false, error: `第 ${i / 2 + 1} 段不是水平/垂直(dx=${x2 - x1}, dy=${y2 - y1})`, primitives: [], note: '' }
1578
+ }
1579
+ }
1580
+ const kind = args?.kind === 'pcb' ? 'pcb' : 'sch'
1581
+ const net = typeof args?.net === 'string' && args.net !== '' ? args.net : ''
1582
+ await backend.refresh()
1583
+ if (!statusOf(backend).connected) {
1584
+ pushActivity({ sid, tool: 'eda_wire', action: '画导线', ok: false, error: '官方桥未连接(门控拦截)' })
1585
+ return { ok: false, error: '官方桥未连接:请先 eda_backend_connect(并确认扩展 Run API Gateway 已装且开启「外部交互」)。', primitives: [], note: '' }
1586
+ }
1587
+ const code = kind === 'pcb' ? pcbWireCode(pts, net) : schWireCode(pts, net)
1588
+ const actId = pushPendingActivity({ sid, tool: 'eda_wire', action: '画导线', code })
1589
+ const t0 = Date.now()
1590
+ const r = await backend.execute(code, 30000)
1591
+ if (r.ok) {
1592
+ let j = null
1593
+ try { j = JSON.parse(r.result) } catch { j = null }
1594
+ if (j?.ok === true) {
1595
+ updateActivity(actId, { status: 'done', ok: true, action: '画导线', result: `${(j.primitives ?? []).length} 段`, durationMs: Date.now() - t0 })
1596
+ return { ok: true, primitives: j.primitives ?? [], note: j.note ?? '' }
1597
+ }
1598
+ updateActivity(actId, { status: 'error', ok: false, action: '画导线', error: j?.error || r.error, durationMs: Date.now() - t0 })
1599
+ return { ok: false, error: j?.error || r.error || '画导线失败', primitives: [], note: '' }
1600
+ }
1601
+ updateActivity(actId, { status: 'error', ok: false, action: '画导线', error: r.error, durationMs: Date.now() - t0 })
1602
+ return { ok: false, error: r.error, primitives: [], note: '' }
1603
+ },
1604
+ })
1605
+ }
1606
+
1607
+ /** eda_netflag:网络标志(sch)。 */
1608
+ function edaNetflagTool(backend) {
1609
+ const TYPES = ['Power', 'Ground', 'AnalogGround', 'ProtectGround']
1610
+ return defineTool({
1611
+ name: 'eda_netflag',
1612
+ description: 'Place a net flag (power / ground) on the schematic at (x,y). type must be one of Power / Ground / AnalogGround / ProtectGround. ' +
1613
+ 'Triggers: 放网络标志, 电源/地, net flag, connect to GND.',
1614
+ parameters: {
1615
+ type: { type: 'string', required: true, description: "'Power'|'Ground'|'AnalogGround'|'ProtectGround'" },
1616
+ net: { type: 'string', required: true, description: '网络名' },
1617
+ x: { type: 'integer', required: true, description: 'X 坐标(10mil)' },
1618
+ y: { type: 'integer', required: true, description: 'Y 坐标(10mil)' },
1619
+ },
1620
+ output: {
1621
+ schema: {
1622
+ type: 'object',
1623
+ additionalProperties: false,
1624
+ properties: {
1625
+ ok: { type: 'boolean', required: true },
1626
+ id: { type: 'string' },
1627
+ net: { type: 'string' },
1628
+ x: { type: 'integer' },
1629
+ y: { type: 'integer' },
1630
+ error: { type: 'string' },
1631
+ },
1632
+ },
1633
+ render: (_args, value) => {
1634
+ if (value?.ok !== true) return [{ type: 'text', text: `网络标志失败: ${value?.error ?? '?'}` }]
1635
+ return [{ type: 'text', text: `✅ 网络标志「${value.net}」@(${value.x}, ${value.y})${value.id ? ' id: ' + value.id : ''}` }]
1636
+ },
1637
+ },
1638
+ async execute(args, exec) {
1639
+ const sid = sessionIdOf(exec)
1640
+ const type = args?.type
1641
+ if (typeof type !== 'string' || !TYPES.includes(type)) {
1642
+ return { ok: false, error: `type 必须是 ${TYPES.join('/')}`, id: '', net: '', x: 0, y: 0 }
1643
+ }
1644
+ const net = typeof args?.net === 'string' && args.net.trim() !== '' ? args.net.trim() : ''
1645
+ if (net === '') {
1646
+ return { ok: false, error: 'net 不能为空', id: '', net: '', x: 0, y: 0 }
1647
+ }
1648
+ const x = Number(args?.x); const y = Number(args?.y)
1649
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
1650
+ return { ok: false, error: 'x/y 必须是坐标', id: '', net: '', x: 0, y: 0 }
1651
+ }
1652
+ await backend.refresh()
1653
+ if (!statusOf(backend).connected) {
1654
+ pushActivity({ sid, tool: 'eda_netflag', action: '网络标志', ok: false, error: '官方桥未连接(门控拦截)' })
1655
+ return { ok: false, error: '官方桥未连接:请先 eda_backend_connect(并确认扩展 Run API Gateway 已装且开启「外部交互」)。', id: '', net, x, y }
1656
+ }
1657
+ const code = schNetflagCode(type, net, x, y)
1658
+ const actId = pushPendingActivity({ sid, tool: 'eda_netflag', action: '网络标志', code })
1659
+ const t0 = Date.now()
1660
+ const r = await backend.execute(code, 30000)
1661
+ if (r.ok) {
1662
+ let j = null
1663
+ try { j = JSON.parse(r.result) } catch { j = null }
1664
+ if (j?.ok === true) {
1665
+ updateActivity(actId, { status: 'done', ok: true, action: '网络标志', result: `${j.net}@(${j.x},${j.y})`, durationMs: Date.now() - t0 })
1666
+ return { ok: true, id: j.id ?? '', net: j.net ?? net, x: Number(j.x ?? x), y: Number(j.y ?? y) }
1667
+ }
1668
+ updateActivity(actId, { status: 'error', ok: false, action: '网络标志', error: j?.error || r.error, durationMs: Date.now() - t0 })
1669
+ return { ok: false, error: j?.error || r.error || '网络标志失败', id: '', net, x, y }
1670
+ }
1671
+ updateActivity(actId, { status: 'error', ok: false, action: '网络标志', error: r.error, durationMs: Date.now() - t0 })
1672
+ return { ok: false, error: r.error, id: '', net, x, y }
1673
+ },
1674
+ })
1675
+ }
1676
+
1677
+ /** eda_save:保存当前文档(sch/pcb 自动判断)。 */
1678
+ function edaSaveTool(backend) {
1679
+ return defineTool({
1680
+ name: 'eda_save',
1681
+ description: 'Save the CURRENT document (auto-detects schematic vs PCB via documentType: 3 = PCB). ' +
1682
+ 'Triggers: 保存当前文档, save document, 保存画板.',
1683
+ parameters: {},
1684
+ output: {
1685
+ schema: {
1686
+ type: 'object',
1687
+ additionalProperties: false,
1688
+ properties: {
1689
+ ok: { type: 'boolean', required: true },
1690
+ saved: { type: 'boolean' },
1691
+ docType: { type: 'integer' },
1692
+ error: { type: 'string' },
1693
+ },
1694
+ },
1695
+ render: (_args, value) => {
1696
+ if (value?.ok !== true) return [{ type: 'text', text: `保存失败: ${value?.error ?? '?'}` }]
1697
+ return [{ type: 'text', text: `✅ 已保存(${value.docType === 3 ? 'PCB' : '原理图'}): ${value.saved === true ? '成功' : '失败'}` }]
1698
+ },
1699
+ },
1700
+ async execute(_args, exec) {
1701
+ const sid = sessionIdOf(exec)
1702
+ await backend.refresh()
1703
+ if (!statusOf(backend).connected) {
1704
+ pushActivity({ sid, tool: 'eda_save', action: '保存文档', ok: false, error: '官方桥未连接(门控拦截)' })
1705
+ return { ok: false, error: '官方桥未连接:请先 eda_backend_connect(并确认扩展 Run API Gateway 已装且开启「外部交互」)。', saved: false, docType: 0 }
1706
+ }
1707
+ const code = saveDocCode()
1708
+ const actId = pushPendingActivity({ sid, tool: 'eda_save', action: '保存文档', code })
1709
+ const t0 = Date.now()
1710
+ const r = await backend.execute(code, 30000)
1711
+ if (r.ok) {
1712
+ let j = null
1713
+ try { j = JSON.parse(r.result) } catch { j = null }
1714
+ if (j?.ok === true) {
1715
+ updateActivity(actId, { status: 'done', ok: true, action: '保存文档', result: j.saved ? '已保存' : '保存失败', durationMs: Date.now() - t0 })
1716
+ return { ok: true, saved: j.saved === true, docType: Number(j.docType ?? 0) }
1717
+ }
1718
+ updateActivity(actId, { status: 'error', ok: false, action: '保存文档', error: j?.error || r.error, durationMs: Date.now() - t0 })
1719
+ return { ok: false, error: j?.error || r.error || '保存失败', saved: false, docType: 0 }
1720
+ }
1721
+ updateActivity(actId, { status: 'error', ok: false, action: '保存文档', error: r.error, durationMs: Date.now() - t0 })
1722
+ return { ok: false, error: r.error, saved: false, docType: 0 }
1723
+ },
1724
+ })
1725
+ }
1726
+
1327
1727
  /** -------------------------------------------------------------- apply */
1328
1728
 
1329
1729
  export function apply(ctx, config) {
@@ -1397,6 +1797,10 @@ export function apply(ctx, config) {
1397
1797
  edaCapabilitiesTool(),
1398
1798
  edaSkillReadTool(),
1399
1799
  edaBoardOverviewTool(backend),
1800
+ edaPlaceTool(backend),
1801
+ edaWireTool(backend),
1802
+ edaNetflagTool(backend),
1803
+ edaSaveTool(backend),
1400
1804
  edaTraceTool(backend),
1401
1805
  edaVerifyTool(edaHandle, backend),
1402
1806
  edaSnapshotTool(edaHandle, backend),