@windypro-rourou/dsh-eda 0.1.9 → 0.1.10

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 (3) hide show
  1. package/README.md +6 -3
  2. package/lib/index.js +286 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -52,18 +52,21 @@ git clone https://github.com/WindyPro-rourou/JCL-EDA.git node_modules/@windypro-
52
52
  └─ package.json ← @windypro-rourou/dsh-eda(files 含 lib/src/patch;npm i github:… 即装)
53
53
  ```
54
54
 
55
- ## 已注册的 agent 工具(21 个)
55
+ ## 已注册的 agent 工具(24 个)
56
56
 
57
57
  | 工具 | 用途 |
58
58
  |---|---|
59
- | `eda_status` | 插件/官方桥状态(装没装 / 端口 / 是否就绪) |
59
+ | `eda_status` | 插件/官方桥状态(装没装 / 端口 / 是否就绪 / 新版本提示) |
60
60
  | `eda_template_list` / `eda_translate_request` | 模板卡目录 / 中文需求→结构化草稿(离线兜底用) |
61
61
  | `eda_generate_schematic_json` | **离线兜底**:需求/模板 → 可导入标准版原理图 JSON + 结构/连通性校验(**9 种符号**:电阻/LED/电容/二极管/开关/电感/晶振/电池/保险丝 + 网络标签) |
62
62
  | `eda_bridge_install` / `eda_backend_connect` | 一键安装官方桥 / 启动·连接官方桥 |
63
63
  | `eda_exec` | **云端实时生成的双手**:执行任意官方 `eda.*` 代码(需已连接;自动记录新建图元→可撤回) |
64
+ | `eda_task` | **后台任务**:长操作(自动布线/大导出/批量写回)提交与查询(桥异步补丁:/tasks、/routing,超时返回 taskId 且迟到结果保留) |
65
+ | `eda_export` | **打样交付包**:一键导出 Gerber zip + BOM + 坐标文件(Pick&Place)+ PCB 信息到本地目录 |
66
+ | `eda_sch_to_pcb` | **原理图→PCB 复制**:读原理图器件 `getState_Component()` → 在 PCB 网格放置(可靠替代不可用的 `importChanges`) |
64
67
  | `eda_place` / `eda_wire` / `eda_netflag` / `eda_save` | **确定性封装**:搜索+定位+放置一体(返回引脚坐标)/ 引脚级连线(自动校验)/ 电源地网络标志 / 保存当前文档 |
65
68
  | `eda_pick_spot` | **框内定位**:读页面尺寸+已有图元 → 返回互不冲突的框内网格空位(sch/pcb 双模式) |
66
- | `eda_capabilities` | **能力清单**:官方 eda.* API 结构化目录(域/方法/用法/坑/实测片段)——agent 主动查询 |
69
+ | `eda_capabilities` | **能力清单**:官方 eda.* API 结构化目录(含层能力矩阵/全链路实测/坑)——agent 主动查询 |
67
70
  | `eda_board_overview` | **画板全览**:当前文档图元/网络/页面尺寸摘要(agent 的眼睛,画前画后都可用) |
68
71
  | `eda_trace` | **现场截图**:缩放适配 → PNG 存 `~/.dsh/eda/shots/`(效果确认/视觉留档) |
69
72
  | `eda_snapshot` | **紧急保存**:画板现场(`.epro2`+SVG 预览+网表/BOM)+ 动作日志 → `~/.dsh/eda/snapshots/` |
package/lib/index.js CHANGED
@@ -1866,6 +1866,290 @@ function edaTaskTool(backend) {
1866
1866
  })
1867
1867
  }
1868
1868
 
1869
+
1870
+ /**
1871
+ * eda_export:一键导出打样交付包(Gerber / BOM / 坐标 / PCB 信息)到本地目录。
1872
+ * 注意:getManufactureData() 会挂起——这里逐个导出;Gerber 用无参 getGerberFile()(约 2-3s)。
1873
+ */
1874
+ function edaExportTool(backend) {
1875
+ return defineTool({
1876
+ name: 'eda_export',
1877
+ description: 'Export a fabrication-ready delivery package for the CURRENT PCB to a local folder: Gerber zip, BOM xlsx, ' +
1878
+ 'pick-and-place xlsx, and PCB info. Each kind can be requested individually. ' +
1879
+ 'Requires the official bridge connected and a PCB document (auto-opens the project PCB if a schematic is active). ' +
1880
+ 'Triggers: 导出打样包, export gerber, 打样交付, 导出 BOM/坐标, fab package.',
1881
+ parameters: {
1882
+ kinds: { type: 'string', description: '逗号分隔:gerber,bom,pnp,info(默认全部)' },
1883
+ dir: { type: 'string', description: '可选:本地输出目录(默认 ~/.dsh/eda/export/<时间戳>/)' },
1884
+ },
1885
+ output: {
1886
+ schema: {
1887
+ type: 'object',
1888
+ additionalProperties: false,
1889
+ properties: {
1890
+ ok: { type: 'boolean', required: true },
1891
+ dir: { type: 'string' },
1892
+ files: {
1893
+ type: 'array',
1894
+ items: {
1895
+ type: 'object',
1896
+ additionalProperties: false,
1897
+ properties: {
1898
+ kind: { type: 'string', required: true },
1899
+ name: { type: 'string', required: true },
1900
+ size: { type: 'integer', required: true },
1901
+ path: { type: 'string', required: true },
1902
+ },
1903
+ },
1904
+ },
1905
+ errors: { type: 'array', items: { type: 'string' } },
1906
+ error: { type: 'string' },
1907
+ },
1908
+ },
1909
+ render: (_args, value) => {
1910
+ if (value?.ok !== true) return [{ type: 'text', text: '导出失败: ' + (value?.error ?? '?') }]
1911
+ const lines = ['✅ 打样交付包已导出到 ' + value.dir + '(' + (value.files?.length ?? 0) + ' 个文件)']
1912
+ for (const f of value.files ?? []) lines.push(' ' + f.kind.padEnd(7) + ' ' + f.name + ' (' + (f.size / 1024).toFixed(1) + 'KB) → ' + f.path)
1913
+ if ((value.errors ?? []).length > 0) lines.push('⚠ 部分失败: ' + value.errors.join('; '))
1914
+ return [{ type: 'text', text: lines.join('\n') }]
1915
+ },
1916
+ },
1917
+ async execute(args, exec) {
1918
+ const sid = sessionIdOf(exec)
1919
+ await backend.refresh()
1920
+ if (!statusOf(backend).connected) {
1921
+ pushActivity({ sid, tool: 'eda_export', action: '导出打样包', ok: false, error: '官方桥未连接(门控拦截)' })
1922
+ return { ok: false, error: '官方桥未连接:请先 eda_backend_connect(并确认扩展已开启「外部交互」)。', dir: '', files: [], errors: [] }
1923
+ }
1924
+ const kinds = String(args?.kinds ?? '').split(',').map((s) => s.trim().toLowerCase()).filter((s) => ['gerber', 'bom', 'pnp', 'info'].includes(s))
1925
+ const useKinds = kinds.length > 0 ? kinds : ['gerber', 'bom', 'pnp', 'info']
1926
+ const dir = typeof args?.dir === 'string' && args.dir.trim() !== ''
1927
+ ? args.dir
1928
+ : join(homedir(), '.dsh', 'eda', 'export', new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19))
1929
+ const actId = pushPendingActivity({ sid, tool: 'eda_export', action: '导出打样包', code: 'kinds=' + useKinds.join(',') })
1930
+ const t0 = Date.now()
1931
+ try {
1932
+ // 1) 确保当前是 PCB(在原理图时自动打开工程 PCB)
1933
+ const docR = await backend.execute('return await eda.dmt_SelectControl.getCurrentDocumentInfo();', 15000)
1934
+ let doc = null
1935
+ try { doc = JSON.parse(docR.result ?? 'null') } catch (e) { doc = null }
1936
+ if (doc === null || doc.documentType !== 3) {
1937
+ const openR = await backend.execute('return await (async () => {' +
1938
+ 'const proj = await eda.dmt_Project.getCurrentProjectInfo();' +
1939
+ 'const pcbUuid = proj?.data?.[0]?.pcb?.uuid;' +
1940
+ 'if (!pcbUuid) return { ok: false, err: "当前工程没有 PCB" };' +
1941
+ 'await eda.dmt_EditorControl.openDocument(pcbUuid);' +
1942
+ 'return { ok: true, pcbUuid }; })();', 20000)
1943
+ let open = null
1944
+ try { open = JSON.parse(openR.result ?? 'null') } catch (e) { open = null }
1945
+ if (!openR.ok || !open || open.ok !== true) {
1946
+ updateActivity(actId, { status: 'error', ok: false, action: '导出打样包', error: '无法打开 PCB 文档', durationMs: Date.now() - t0 })
1947
+ return { ok: false, error: '当前工程没有可导出的 PCB 文档', dir, files: [], errors: [] }
1948
+ }
1949
+ await new Promise((resolve) => setTimeout(resolve, 1800))
1950
+ }
1951
+ // 2) 执行导出(逐个 grab;Gerber 用无参)
1952
+ const code = 'return await (async () => {' +
1953
+ 'const kinds = ' + JSON.stringify(useKinds) + ';' +
1954
+ 'const out = {};' +
1955
+ 'const grab = async (kind, fn) => {' +
1956
+ ' try {' +
1957
+ ' const f = await fn();' +
1958
+ ' if (!f) { out[kind] = { ok: false, err: "null File" }; return; }' +
1959
+ ' const bytes = new Uint8Array(await f.arrayBuffer());' +
1960
+ ' let bin = "";' +
1961
+ ' for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));' +
1962
+ ' out[kind] = { ok: true, name: f.name, size: f.size, b64: btoa(bin) };' +
1963
+ ' } catch (e) { out[kind] = { ok: false, err: String(e).slice(0, 140) }; }' +
1964
+ '};' +
1965
+ 'if (kinds.includes("gerber")) await grab("gerber", () => eda.pcb_ManufactureData.getGerberFile());' +
1966
+ 'if (kinds.includes("bom")) await grab("bom", () => eda.pcb_ManufactureData.getBomFile());' +
1967
+ 'if (kinds.includes("pnp")) await grab("pnp", () => eda.pcb_ManufactureData.getPickAndPlaceFile());' +
1968
+ 'if (kinds.includes("info")) await grab("info", () => eda.pcb_ManufactureData.getPcbInfoFile());' +
1969
+ 'return out; })();'
1970
+ const r = await backend.execute(code, 90000)
1971
+ if (!r.ok) {
1972
+ updateActivity(actId, { status: 'error', ok: false, action: '导出打样包', error: r.error, durationMs: Date.now() - t0 })
1973
+ return { ok: false, error: r.error, dir, files: [], errors: [] }
1974
+ }
1975
+ let parsed = null
1976
+ try { parsed = JSON.parse(r.result ?? 'null') } catch (e) { parsed = null }
1977
+ if (!parsed) return { ok: false, error: '导出结果解析失败', dir, files: [], errors: [] }
1978
+ // 3) 解码写本地文件
1979
+ await fsp.mkdir(dir, { recursive: true })
1980
+ const files = []
1981
+ const errors = []
1982
+ for (const [kind, v] of Object.entries(parsed)) {
1983
+ if (!v || v.ok !== true) { errors.push(kind + ': ' + (v?.err ?? 'failed')); continue }
1984
+ const safeName = String(v.name ?? kind).replace(/[\\/:*?"<>|]/g, '_')
1985
+ const path = join(dir, safeName)
1986
+ await fsp.writeFile(path, Buffer.from(String(v.b64 ?? ''), 'base64'))
1987
+ files.push({ kind, name: safeName, size: Number(v.size ?? 0), path })
1988
+ }
1989
+ updateActivity(actId, {
1990
+ status: files.length > 0 ? 'done' : 'error',
1991
+ ok: files.length > 0, action: '导出打样包',
1992
+ result: files.map((f) => f.name).join(', ').slice(0, 200),
1993
+ error: errors.join('; ').slice(0, 200), durationMs: Date.now() - t0,
1994
+ })
1995
+ return { ok: files.length > 0, dir, files, errors, error: files.length > 0 ? '' : (errors.join('; ') || '导出失败') }
1996
+ } catch (err) {
1997
+ const msg = err instanceof Error ? err.message : String(err)
1998
+ updateActivity(actId, { status: 'error', ok: false, action: '导出打样包', error: msg, durationMs: Date.now() - t0 })
1999
+ return { ok: false, error: msg, dir, files: [], errors: [] }
2000
+ }
2001
+ },
2002
+ })
2003
+ }
2004
+
2005
+ /**
2006
+ * eda_sch_to_pcb:把当前原理图的器件复制到 PCB(替代不可靠的 importChanges)。
2007
+ * 读原理图元件 → getState_Component() 取 device → 在 PCB 网格放置(layer 1)。
2008
+ */
2009
+ function edaSchToPcbTool(backend) {
2010
+ return defineTool({
2011
+ name: 'eda_sch_to_pcb',
2012
+ description: 'Copy the components of the CURRENT schematic onto the project PCB (direct placement). ' +
2013
+ 'This is the reliable replacement for the unreliable PCB_Document.importChanges() (returns true but places nothing). ' +
2014
+ 'Reads each schematic component via getState_Component(), opens the project PCB and places it on layer 1 in a grid. ' +
2015
+ 'Triggers: 原理图转PCB, 复制器件到PCB, sch to pcb, 同步原理图到PCB.',
2016
+ parameters: {
2017
+ spacing: { type: 'integer', description: '可选:网格间距(mil,默认 900)' },
2018
+ },
2019
+ output: {
2020
+ schema: {
2021
+ type: 'object',
2022
+ additionalProperties: false,
2023
+ properties: {
2024
+ ok: { type: 'boolean', required: true },
2025
+ pcbUuid: { type: 'string' },
2026
+ total: { type: 'integer' },
2027
+ placedOk: { type: 'integer' },
2028
+ placed: {
2029
+ type: 'array',
2030
+ items: {
2031
+ type: 'object',
2032
+ additionalProperties: false,
2033
+ properties: {
2034
+ designator: { type: 'string' },
2035
+ ok: { type: 'boolean', required: true },
2036
+ uuid: { type: 'string' },
2037
+ err: { type: 'string' },
2038
+ },
2039
+ },
2040
+ },
2041
+ note: { type: 'string' },
2042
+ error: { type: 'string' },
2043
+ },
2044
+ },
2045
+ render: (_args, value) => {
2046
+ if (value?.ok !== true) return [{ type: 'text', text: '原理图转 PCB 失败: ' + (value?.error ?? '?') }]
2047
+ const lines = ['✅ 原理图 → PCB:' + (value.placedOk ?? 0) + '/' + (value.total ?? 0) + ' 个器件已放置到 ' + (value.pcbUuid ?? '')]
2048
+ for (const p of value.placed ?? []) if (!p.ok && p.err) lines.push(' ⚠ ' + (p.designator ?? '?') + ': ' + p.err)
2049
+ if (value.note) lines.push(value.note)
2050
+ return [{ type: 'text', text: lines.join('\n') }]
2051
+ },
2052
+ },
2053
+ async execute(args, exec) {
2054
+ const sid = sessionIdOf(exec)
2055
+ await backend.refresh()
2056
+ if (!statusOf(backend).connected) {
2057
+ pushActivity({ sid, tool: 'eda_sch_to_pcb', action: '原理图转PCB', ok: false, error: '官方桥未连接(门控拦截)' })
2058
+ return { ok: false, pcbUuid: '', total: 0, placedOk: 0, placed: [], note: '', error: '官方桥未连接:请先 eda_backend_connect。' }
2059
+ }
2060
+ const spacing = Number.isFinite(args?.spacing) ? Math.max(300, Math.min(3000, args.spacing)) : 900
2061
+ const actId = pushPendingActivity({ sid, tool: 'eda_sch_to_pcb', action: '原理图转PCB', code: 'spacing=' + spacing })
2062
+ const t0 = Date.now()
2063
+ try {
2064
+ const pre = await backend.execute('return await (async () => {' +
2065
+ 'const comps = await eda.sch_PrimitiveComponent.getAll();' +
2066
+ 'const list = [];' +
2067
+ 'for (const c of (comps || [])) {' +
2068
+ ' let type = ""; try { type = c.getState_ComponentType(); } catch (e) {}' +
2069
+ ' if (["sheet","netflag","netport","wire","bus","text"].includes(type)) continue;' +
2070
+ ' let device = null; try { device = c.getState_Component ? c.getState_Component() : null; } catch (e) {}' +
2071
+ ' if (!device) continue;' +
2072
+ ' list.push({ id: (() => { try { return c.getState_PrimitiveId(); } catch (e) { return ""; } })(),' +
2073
+ ' designator: (() => { try { return c.getState_Designator(); } catch (e) { return ""; } })() });' +
2074
+ '}' +
2075
+ 'const proj = await eda.dmt_Project.getCurrentProjectInfo();' +
2076
+ 'const pcbUuid = proj?.data?.[0]?.pcb?.uuid;' +
2077
+ 'return { ok: true, count: list.length, pcbUuid }; })();', 30000)
2078
+ let preData = null
2079
+ try { preData = JSON.parse(pre.result ?? 'null') } catch (e) { preData = null }
2080
+ if (!pre.ok || !preData || preData.ok !== true) {
2081
+ updateActivity(actId, { status: 'error', ok: false, action: '原理图转PCB', error: '读取原理图器件失败', durationMs: Date.now() - t0 })
2082
+ return { ok: false, pcbUuid: '', total: 0, placedOk: 0, placed: [], note: '', error: '读取原理图器件失败:' + (pre.error ?? '') }
2083
+ }
2084
+ if (!preData.pcbUuid) {
2085
+ updateActivity(actId, { status: 'error', ok: false, action: '原理图转PCB', error: '当前工程没有 PCB', durationMs: Date.now() - t0 })
2086
+ return { ok: false, pcbUuid: '', total: Number(preData.count ?? 0), placedOk: 0, placed: [], note: '', error: '当前工程没有 PCB(先 createPcb 建板)' }
2087
+ }
2088
+ const code = 'return await (async () => {' +
2089
+ 'const sleep = (ms) => new Promise(r => setTimeout(r, ms));' +
2090
+ 'const comps = await eda.sch_PrimitiveComponent.getAll();' +
2091
+ 'const list = [];' +
2092
+ 'for (const c of (comps || [])) {' +
2093
+ ' let type = ""; try { type = c.getState_ComponentType(); } catch (e) {}' +
2094
+ ' if (["sheet","netflag","netport","wire","bus","text"].includes(type)) continue;' +
2095
+ ' let device = null; try { device = c.getState_Component ? c.getState_Component() : null; } catch (e) {}' +
2096
+ ' if (!device) continue;' +
2097
+ ' list.push({ designator: (() => { try { return c.getState_Designator(); } catch (e) { return ""; } })(), device });' +
2098
+ '}' +
2099
+ 'const proj = await eda.dmt_Project.getCurrentProjectInfo();' +
2100
+ 'const pcbUuid = proj?.data?.[0]?.pcb?.uuid;' +
2101
+ 'if (!pcbUuid) return { ok: false, err: "no pcb" };' +
2102
+ 'await eda.dmt_EditorControl.openDocument(pcbUuid);' +
2103
+ 'await sleep(1800);' +
2104
+ 'const spacing = SPACING;' +
2105
+ 'const placed = [];' +
2106
+ 'let x = 1500, y = 1200, col = 0;' +
2107
+ 'for (const it of list) {' +
2108
+ ' try {' +
2109
+ ' const comp = await eda.pcb_PrimitiveComponent.create(it.device, 1, x, y);' +
2110
+ ' placed.push({ designator: String(it.designator || ""), ok: !!comp, uuid: comp && comp.getState_PrimitiveId ? comp.getState_PrimitiveId() : "" });' +
2111
+ ' } catch (e) { placed.push({ designator: String(it.designator || ""), ok: false, err: String(e).slice(0, 80) }); }' +
2112
+ ' col++; if (col % 6 === 0) { x = 1500; y += spacing; } else x += spacing;' +
2113
+ ' await sleep(120);' +
2114
+ '}' +
2115
+ 'try { await eda.pcb_Document.save(); } catch (e) {}' +
2116
+ 'return { ok: true, pcbUuid, total: list.length, placedOk: placed.filter(p => p.ok).length, placed }; })();'
2117
+ const r = await backend.execute(code.replace('SPACING', String(spacing)), 120000)
2118
+ if (!r.ok) {
2119
+ updateActivity(actId, { status: 'error', ok: false, action: '原理图转PCB', error: r.error, durationMs: Date.now() - t0 })
2120
+ return { ok: false, pcbUuid: '', total: 0, placedOk: 0, placed: [], note: '', error: r.error }
2121
+ }
2122
+ let parsed = null
2123
+ try { parsed = JSON.parse(r.result ?? 'null') } catch (e) { parsed = null }
2124
+ if (!parsed) return { ok: false, pcbUuid: '', total: 0, placedOk: 0, placed: [], note: '', error: '结果解析失败' }
2125
+ if (parsed.ok !== true) {
2126
+ updateActivity(actId, { status: 'error', ok: false, action: '原理图转PCB', error: String(parsed.err ?? 'failed'), durationMs: Date.now() - t0 })
2127
+ return { ok: false, pcbUuid: '', total: Number(parsed.total ?? 0), placedOk: 0, placed: [], note: '', error: String(parsed.err ?? '原理图转 PCB 失败') }
2128
+ }
2129
+ const placed = (parsed.placed ?? []).map((p) => {
2130
+ const row = { designator: String(p.designator ?? ''), ok: p.ok === true }
2131
+ if (p.uuid) row.uuid = String(p.uuid)
2132
+ if (p.err) row.err = String(p.err)
2133
+ return row
2134
+ })
2135
+ updateActivity(actId, {
2136
+ status: 'done', ok: true, action: '原理图转PCB',
2137
+ result: (parsed.placedOk ?? 0) + '/' + (parsed.total ?? 0) + ' 已放置', error: '', durationMs: Date.now() - t0,
2138
+ })
2139
+ return {
2140
+ ok: true, pcbUuid: String(parsed.pcbUuid ?? ''), total: Number(parsed.total ?? 0),
2141
+ placedOk: Number(parsed.placedOk ?? 0), placed,
2142
+ note: '放置坐标是元件原点(封装可能偏移);位号沿用原理图但 PCB 上可能为空,需人工确认。',
2143
+ }
2144
+ } catch (err) {
2145
+ const msg = err instanceof Error ? err.message : String(err)
2146
+ updateActivity(actId, { status: 'error', ok: false, action: '原理图转PCB', error: msg, durationMs: Date.now() - t0 })
2147
+ return { ok: false, pcbUuid: '', total: 0, placedOk: 0, placed: [], note: '', error: msg }
2148
+ }
2149
+ },
2150
+ })
2151
+ }
2152
+
1869
2153
  /** eda_save:保存当前文档(sch/pcb 自动判断)。 */
1870
2154
  function edaSaveTool(backend) {
1871
2155
  return defineTool({
@@ -1991,6 +2275,8 @@ export function apply(ctx, config) {
1991
2275
  edaBackendConnectTool(edaHandle, backend),
1992
2276
  edaExecTool(backend),
1993
2277
  edaTaskTool(backend),
2278
+ edaExportTool(backend),
2279
+ edaSchToPcbTool(backend),
1994
2280
  edaPickSpotTool(backend),
1995
2281
  edaCapabilitiesTool(),
1996
2282
  edaSkillReadTool(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windypro-rourou/dsh-eda",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "dsh-lichuang-eda (JCL-EDA): 嘉立创 EDA 助手 DSH 插件 —— 官方 eda.* API 云端实时生成(原理图/PCB)+ 离线标准版 JSON 兜底 + 面板记录式时间线(撤回/清空/紧急保存)。开箱即用:npm i 后 cordis.patch.yml insert 一行即挂载。",
6
6
  "packageManager": "pnpm@11.22.0",