@windypro-rourou/dsh-eda 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * dsh-lichuang-eda — one-click installer for the OFFICIAL EasyEDA bridge.
3
+ *
4
+ * Vendors the official `easyeda/easyeda-api-skill` (Skill easyeda-api,
5
+ * author JLCEDA, MIT) into ~/.dsh/eda/bridge/: downloads the source archive,
6
+ * extracts it, runs `npm install` (its only dependency is ws), and leaves the
7
+ * official Bridge Server at <dir>/scripts/bridge-server.mjs.
8
+ *
9
+ * No third-party backend (easyeda-agent etc.) is involved.
10
+ */
11
+
12
+ import { createHash } from 'node:crypto'
13
+ import { promises as fsp } from 'node:fs'
14
+ import { existsSync } from 'node:fs'
15
+ import { homedir } from 'node:os'
16
+ import { join } from 'node:path'
17
+ import { spawn } from 'node:child_process'
18
+
19
+ export const DEFAULT_VERSION = 'main' // branch of the official skill repo
20
+ export const OFFICIAL_REPO = 'easyeda/easyeda-api-skill'
21
+ export const OFFICIAL_EXT_URL = 'https://jlc-ext.com/item/oshwhub/run-api-gateway' // 官方扩展 Run API Gateway
22
+
23
+ export function bridgeDir() { return join(homedir(), '.dsh', 'eda', 'bridge') }
24
+
25
+ /** Absolute path of the official bridge server script. */
26
+ export function bridgeScriptPath() {
27
+ return join(bridgeDir(), 'scripts', 'bridge-server.mjs')
28
+ }
29
+
30
+ /** True when the official bridge was installed by this plugin. */
31
+ export function isBridgeInstalled() {
32
+ return existsSync(join(bridgeDir(), 'package.json')) && existsSync(join(bridgeDir(), 'scripts', 'bridge-server.mjs')) && existsSync(join(bridgeDir(), 'node_modules', 'ws'))
33
+ }
34
+
35
+ const archiveUrl = (version) => `https://github.com/${OFFICIAL_REPO}/archive/refs/heads/${version}.tar.gz`
36
+
37
+ function run(cmd, args, opts = {}) {
38
+ return new Promise((resolve, reject) => {
39
+ const child = spawn(cmd, args, { ...opts, windowsHide: true })
40
+ const err = []
41
+ child.stderr?.on('data', (d) => err.push(String(d)))
42
+ child.on('exit', (code) => {
43
+ if (code === 0) resolve({ ok: true })
44
+ else reject(new Error(`${cmd} ${args.join(' ')} 退出码 ${code}:${err.join('').slice(-300)}`))
45
+ })
46
+ child.on('error', reject)
47
+ })
48
+ }
49
+
50
+ async function extractTarGz(archive, destTmp) {
51
+ // bsdtar (Windows 10+) / GNU tar (macOS/Linux) both handle .tar.gz.
52
+ await fsp.mkdir(destTmp, { recursive: true })
53
+ try {
54
+ await run('tar', ['-xf', archive, '-C', destTmp], { stdio: ['ignore', 'ignore', 'pipe'] })
55
+ } catch (error) {
56
+ throw new Error('解压失败(需要 tar;Windows 10 1803+ 自带 bsdtar):' + (error instanceof Error ? error.message : String(error)))
57
+ }
58
+ }
59
+
60
+ /** Recursive copy (used to vendor `ws` — the bridge's only runtime dep). */
61
+ async function copyDir(src, dst) {
62
+ const entries = await fsp.readdir(src, { withFileTypes: true })
63
+ await fsp.mkdir(dst, { recursive: true })
64
+ for (const e of entries) {
65
+ const s = join(src, e.name)
66
+ const d = join(dst, e.name)
67
+ if (e.isDirectory()) await copyDir(s, d)
68
+ else await fsp.copyFile(s, d)
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Locate an existing `ws` package on this machine (the DSH host ships it via
74
+ * dsh-logcat's dependency) so we can vendor it WITHOUT npm — this machine has
75
+ * PowerShell ExecutionPolicy blocking `npm.ps1`, and offline copy is faster
76
+ * and never touches the registry.
77
+ */
78
+ export async function findExistingWs() {
79
+ const profiles = join(homedir(), '.dsh', 'profiles')
80
+ const candidates = []
81
+ try {
82
+ for (const p of await fsp.readdir(profiles)) {
83
+ candidates.push(join(profiles, p, 'node_modules', 'ws'))
84
+ }
85
+ } catch { /* profiles dir missing */ }
86
+ // also the running web profile's hoisted location
87
+ for (const c of candidates) {
88
+ try {
89
+ await fsp.access(join(c, 'package.json'))
90
+ return c
91
+ } catch { /* keep looking */ }
92
+ }
93
+ return null
94
+ }
95
+
96
+ async function ensureWs(dir) {
97
+ // 1) offline vendor from the host's `ws` (no npm, no registry, no PS policy).
98
+ const src = await findExistingWs()
99
+ if (src !== null) {
100
+ const dst = join(dir, 'node_modules', 'ws')
101
+ await copyDir(src, dst)
102
+ await fsp.writeFile(join(dir, 'node_modules', 'ws', '.vendored-by-dsh'), 'yes\n', 'utf8')
103
+ return { method: 'vendored', src }
104
+ }
105
+ // 2) fallback: npm (Windows resolves npm.cmd; the .ps1 shim is disabled by policy).
106
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
107
+ await run(npmCmd, ['install', '--prefix', dir, '--no-fund', '--no-audit'], { stdio: ['ignore', 'ignore', 'pipe'] })
108
+ return { method: 'npm', src: npmCmd }
109
+ }
110
+
111
+ /**
112
+ * Download + install the official bridge (easyeda-api-skill).
113
+ * @param {object} opts { version=DEFAULT_VERSION }
114
+ * @returns {Promise<{ok:true,dir,script,version,extUrl,wsMethod}>}
115
+ */
116
+ export async function installOfficialBridge(opts = {}) {
117
+ const version = opts.version ?? DEFAULT_VERSION
118
+ const dir = bridgeDir()
119
+
120
+ // Already installed: never touch it (idempotent — keeps the vendored ws and
121
+ // the live bridge state intact if the user clicks install again).
122
+ if (isBridgeInstalled()) {
123
+ return { ok: true, dir, script: bridgeScriptPath(), version, extUrl: OFFICIAL_EXT_URL, already: true }
124
+ }
125
+
126
+ await fsp.mkdir(dir, { recursive: true })
127
+
128
+ const url = archiveUrl(version)
129
+ const res = await fetch(url, { signal: AbortSignal.timeout(120000), redirect: 'follow' })
130
+ if (!res.ok) throw new Error(`下载官方桥失败:HTTP ${res.status} ${url}`)
131
+ const buf = Buffer.from(await res.arrayBuffer())
132
+
133
+ const tmpRoot = join(homedir(), '.dsh', 'eda', '.tmp-bridge')
134
+ await fsp.rm(tmpRoot, { recursive: true, force: true })
135
+ const archive = join(tmpRoot, 'bridge.tar.gz')
136
+ await fsp.mkdir(tmpRoot, { recursive: true })
137
+ await fsp.writeFile(archive, buf)
138
+ const extracted = join(tmpRoot, 'src')
139
+ await extractTarGz(archive, extracted)
140
+
141
+ // Archive layout: <repo>-main/… — move its contents into place.
142
+ const entries = await fsp.readdir(extracted)
143
+ const inner = entries.length === 1 ? join(extracted, entries[0]) : extracted
144
+ for (const name of await fsp.readdir(inner)) {
145
+ await fsp.rm(join(dir, name), { recursive: true, force: true })
146
+ await fsp.rename(join(inner, name), join(dir, name))
147
+ }
148
+ await fsp.rm(tmpRoot, { recursive: true, force: true })
149
+
150
+ // Install the only runtime dependency (ws): offline vendor preferred.
151
+ await ensureWs(dir)
152
+
153
+ const script = bridgeScriptPath()
154
+ if (!existsSync(script)) throw new Error('官方桥脚本缺失:' + script)
155
+ const ok = isBridgeInstalled()
156
+ if (!ok) throw new Error('官方桥依赖(ws)未就绪:npm 被禁或 ws 不可用。请检查 ExecutionPolicy 或网络。')
157
+ return { ok: true, dir, script, version, extUrl: OFFICIAL_EXT_URL, wsMethod: ok ? 'vendored-or-npm' : 'none' }
158
+ }
159
+
160
+ export function sha256Hex(buf) {
161
+ return createHash('sha256').update(buf).digest('hex')
162
+ }
package/lib/layout.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * dsh-lichuang-eda — 框内定位(layout)核心。
3
+ *
4
+ * 实战固化(esp32_multitool 真机实测):
5
+ * - 原理图坐标单位 10mil,A4 页面 = 1170 × 825(titleBlockData.Width/Height);
6
+ * - 放置必须落在图框内(内边距 ≥ margin),100 单位网格步进;
7
+ * - 与已有图元(含导线采样点)最小间距 minGap(Chebyshev);
8
+ * - 从未占用区域"右上→左下"逐行扫描,同排元素保持同一行/列间距 ≥ grid。
9
+ *
10
+ * 纯函数(可单测):读页面交给工具层(桥端代码在 layout.js 之外)。
11
+ */
12
+
13
+ /** 在页面上为 count 个元件挑选互不冲突的框内网格点。 */
14
+ export function pickSpots({
15
+ pageWidth,
16
+ pageHeight,
17
+ used = [],
18
+ count = 1,
19
+ margin = 80,
20
+ grid = 100,
21
+ minGap = 150,
22
+ }) {
23
+ const spots = []
24
+ const taken = used.map((u) => ({ x: Number(u.x) || 0, y: Number(u.y) || 0 }))
25
+ const conflict = (x, y) =>
26
+ taken.some((u) => Math.abs(u.x - x) < minGap && Math.abs(u.y - y) < minGap) ||
27
+ spots.some((s) => Math.abs(s.x - x) < minGap && Math.abs(s.y - y) < minGap)
28
+
29
+ let y = margin + grid
30
+ while (spots.length < count && y <= pageHeight - margin) {
31
+ let x = pageWidth - margin - grid
32
+ while (x >= margin && spots.length < count) {
33
+ if (!conflict(x, y)) {
34
+ spots.push({ x, y, inside: x >= margin && x <= pageWidth - margin && y >= margin && y <= pageHeight - margin })
35
+ }
36
+ x -= grid
37
+ }
38
+ y += grid
39
+ }
40
+ return spots
41
+ }
42
+
43
+ /** 校验一个点是否在两段式框内(margin 内)。 */
44
+ export function insideFrame({ x, y, pageWidth, pageHeight, margin = 80 }) {
45
+ return x >= margin && x <= pageWidth - margin && y >= margin && y <= pageHeight - margin
46
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * dsh-lichuang-eda — 紧急保存(snapshot)核心。
3
+ *
4
+ * 「如果画板内容没同步上,用户手里也得有最后的工程」——把 agent 在画板上
5
+ * 做的东西完整抓到本地:
6
+ *
7
+ * 1. 画板现场(需官方桥连接,官方 API):
8
+ * - 当前工程信息 / 当前文档信息(dmt_Project / dmt_SelectControl)
9
+ * - 当前文档/工程文件 `.epro2`(专业版原生,完整恢复;SYS_FileManager.getDocumentFile)
10
+ * - 当前图页预览 `preview.svg`(通用:任何浏览器/标准版用户都能看)
11
+ * - 网表 `netlist.json` + BOM `bom.json`(sch_ManufactureData,供核对)
12
+ * 2. agent 动作日志 `log.json`(服务端本就有:每次官方 API 调用记录;
13
+ * 即使画板断连/未连接,最后步骤也能留档)
14
+ * 3. `meta.json` + `README.txt`(打开方法:专业版恢复 / 标准版查看/重建)
15
+ *
16
+ * 设计原则:每一步独立失败(best-effort),一次失败绝不放弃整份快照;
17
+ * 大结果有上限保护;全部写入 ~/.dsh/eda/snapshots/<project>-<stamp>/。
18
+ *
19
+ * 兼容性说明(如实):官方导出仅 epro/epro2(专业版原生)与 PDF/PNG/SVG,
20
+ * 无「标准版 v6 JSON」官方导出通道;因此快照 = 专业版完整恢复 +
21
+ * 通用预览 + 数据文件。标准版可编辑需离线重建(仅简单电路,见
22
+ * eda_generate_schematic_json / 对话内编排)。
23
+ */
24
+
25
+ import { promises as fsp } from 'node:fs'
26
+ import { homedir } from 'node:os'
27
+ import { join } from 'node:path'
28
+
29
+ /** Default snapshot root (~/.dsh/eda/snapshots). */
30
+ export function defaultSnapshotDir() {
31
+ return join(homedir(), '.dsh', 'eda', 'snapshots')
32
+ }
33
+
34
+ /** Per-execute timeout: the bridge itself dies at 30s, stay under it. */
35
+ export const STEP_TIMEOUT_MS = 25000
36
+ /** Refuse to write a doc payload larger than this (sanity guard). */
37
+ export const MAX_PAYLOAD_BYTES = 30 * 1024 * 1024
38
+
39
+ const PROJECT_INFO_CODE = 'return await eda.dmt_Project.getCurrentProjectInfo();'
40
+ const DOC_INFO_CODE = 'return await eda.dmt_SelectControl.getCurrentDocumentInfo();'
41
+
42
+ function docFileCode(baseName) {
43
+ return `return await (async () => {
44
+ const f = await eda.sys_FileManager.getDocumentFile('${baseName}');
45
+ if (!f) return null;
46
+ const text = await f.text();
47
+ return { name: f.name, size: f.size, type: f.type, len: text.length, text };
48
+ })();`
49
+ }
50
+
51
+ function previewSvgCode(baseName) {
52
+ return `return await (async () => {
53
+ const f = await eda.sch_ManufactureData.getExportDocumentFile('${baseName}-preview', 'SVG', undefined, 'Current Schematic');
54
+ if (!f) return null;
55
+ const text = await f.text();
56
+ return { name: f.name, size: f.size, len: text.length, text };
57
+ })();`
58
+ }
59
+
60
+ function netlistCode() {
61
+ return `return await (async () => { const f = await eda.sch_ManufactureData.getNetlistFile(); return f ? await f.text() : null; })();`
62
+ }
63
+
64
+ function bomCode() {
65
+ return `return await (async () => { const f = await eda.sch_ManufactureData.getBomFile(); if (!f) return null; const bytes = new Uint8Array(await f.arrayBuffer()); let bin = ''; for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000)); return { name: f.name, size: f.size, b64: btoa(bin) }; })();`
66
+ }
67
+
68
+ /** Write BOM binary (base64 → bytes to disk as .xlsx). */
69
+ async function writeBom(outDir, files, addError, payload) {
70
+ if (payload == null || typeof payload !== 'object' || typeof payload.b64 !== 'string' || payload.b64 === '') {
71
+ addError('导出BOM', 'no bom file returned')
72
+ return null
73
+ }
74
+ const buf = Buffer.from(payload.b64, 'base64')
75
+ await fsp.mkdir(outDir, { recursive: true })
76
+ const target = join(outDir, 'bom.xlsx')
77
+ await fsp.writeFile(target, buf)
78
+ files.push({ name: 'bom.xlsx', size: buf.length })
79
+ return target
80
+ }
81
+
82
+ /** Parse a likely-JSON result string; return the original when it isn't. */
83
+ export function parseMaybeJson(value) {
84
+ if (typeof value !== 'string') return value
85
+ try { return JSON.parse(value) } catch { return value }
86
+ }
87
+
88
+ /** Filesystem-safe base name (keeps CJK; drops path/illegal chars). */
89
+ export function sanitizeName(name) {
90
+ const cleaned = String(name ?? 'eda-snapshot')
91
+ .replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_')
92
+ .replace(/\s+/g, '-')
93
+ .trim()
94
+ return cleaned.length > 0 && cleaned.length <= 64 ? cleaned : cleaned.slice(0, 64) || 'eda-snapshot'
95
+ }
96
+
97
+ /** The 紧急保存 pipeline. Never throws for per-step failures (returns errors[]). */
98
+ export async function createSnapshot(deps) {
99
+ const {
100
+ execute, // (code, timeoutMs) => Promise<{ok, result, error}>
101
+ activities, // () => array (newest first) — agent action log
102
+ connected, // whether the official bridge is connected now
103
+ dir: dirOverride,
104
+ now = () => new Date(),
105
+ stepTimeoutMs = STEP_TIMEOUT_MS,
106
+ } = deps
107
+
108
+ const stamp = now().toISOString().replace(/[:.]/g, '-').slice(0, 19)
109
+ const outDir = join(dirOverride ?? defaultSnapshotDir(), `snapshot-${stamp}`)
110
+ const files = [] // { name, size }
111
+ const errors = [] // degraded step descriptions
112
+ const project = { info: null, doc: null, docFile: null, preview: null, netlist: null, bom: null }
113
+ const addError = (label, message) => { errors.push(`${label}: ${message}`) }
114
+
115
+ const execJson = async (label, code) => {
116
+ try {
117
+ const r = await execute(code, stepTimeoutMs)
118
+ if (r?.ok) return parseMaybeJson(r.result)
119
+ addError(label, (r?.error ?? 'execute failed').slice(0, 200))
120
+ return null
121
+ } catch (error) {
122
+ addError(label, error instanceof Error ? error.message : String(error))
123
+ return null
124
+ }
125
+ }
126
+
127
+ async function writeFile(name, content) {
128
+ if (typeof content !== 'string' || content === '') return null
129
+ if (content.length > MAX_PAYLOAD_BYTES) {
130
+ addError(`write ${name}`, `payload too large (${content.length} bytes)`)
131
+ return null
132
+ }
133
+ await fsp.mkdir(outDir, { recursive: true })
134
+ const target = join(outDir, name)
135
+ await fsp.writeFile(target, content, 'utf8')
136
+ files.push({ name, size: content.length })
137
+ return target
138
+ }
139
+
140
+ // 1) board state — only when the bridge is actually connected.
141
+ if (connected) {
142
+ project.info = await execJson('工程信息', PROJECT_INFO_CODE)
143
+ project.doc = await execJson('当前文档信息', DOC_INFO_CODE)
144
+
145
+ const projectName = sanitizeName(
146
+ project.info && typeof project.info === 'object' && (project.info.name ?? project.info.projectName)
147
+ ? (project.info.name ?? project.info.projectName)
148
+ : 'eda-snapshot',
149
+ )
150
+ const baseName = projectName.replace(/-$/, '')
151
+
152
+ // 1a) native doc file (epro2) — the full-fidelity restore for 专业版.
153
+ const docFile = await execJson('导出文档文件(epro2)', docFileCode(baseName))
154
+ if (docFile && typeof docFile === 'object' && typeof docFile.text === 'string' && docFile.text !== '') {
155
+ const ext = (typeof docFile.name === 'string' && docFile.name.includes('.')) ? docFile.name.split('.').pop() : 'epro2'
156
+ project.docFile = { name: `${baseName}.${ext}` }
157
+ await writeFile(`${baseName}.${ext}`, docFile.text)
158
+ } else if (docFile === null && errors.length > 0) {
159
+ // already recorded by execJson
160
+ } else {
161
+ addError('导出文档文件(epro2)', 'no document file returned (可能无打开文档/权限不足)')
162
+ }
163
+
164
+ // 1b) universal preview (SVG is text — saves cleanly, opens anywhere).
165
+ const preview = await execJson('导出预览SVG', previewSvgCode(baseName))
166
+ if (preview && typeof preview === 'object' && typeof preview.text === 'string' && preview.text !== '') {
167
+ project.preview = { name: `${baseName}-preview.svg` }
168
+ await writeFile(`${baseName}-preview.svg`, preview.text)
169
+ } else if (preview === null && errors.length > 0) {
170
+ // recorded
171
+ } else {
172
+ addError('导出预览SVG', 'no SVG returned (当前图页可能不是原理图)')
173
+ }
174
+
175
+ // 1c) data files for cross-checking (netlist/BOM are File objects — the
176
+ // snippets above read f.text(), so keep the raw string on disk).
177
+ const netlist = await execJson('导出网表', netlistCode())
178
+ if (typeof netlist === 'string' && netlist !== '') {
179
+ project.netlist = { name: 'netlist.json' }
180
+ await writeFile('netlist.json', netlist)
181
+ } else if (netlist != null && typeof netlist === 'object') {
182
+ project.netlist = { name: 'netlist.json' }
183
+ await writeFile('netlist.json', JSON.stringify(netlist, null, 2))
184
+ } else if (netlist == null) {
185
+ addError('导出网表', 'no netlist file returned')
186
+ }
187
+ const bom = await execJson('导出BOM', bomCode())
188
+ if (typeof bom === 'object' && bom !== null && typeof bom.b64 === 'string') {
189
+ project.bom = { name: 'bom.xlsx' }
190
+ await writeBom(outDir, files, addError, bom)
191
+ } else if (typeof bom === 'object' && bom !== null && Object.keys(bom).length > 0) {
192
+ // Non-binary BOM payload (some netlist-like providers) — keep as JSON.
193
+ project.bom = { name: 'bom.json' }
194
+ await writeFile('bom.json', JSON.stringify(bom, null, 2))
195
+ } else if (bom == null) {
196
+ addError('导出BOM', 'no bom file returned')
197
+ }
198
+ } else {
199
+ addError('画板现场', '官方桥未连接——仅保存 agent 动作日志(步骤留档)')
200
+ }
201
+
202
+ // 2) agent action log — always (server-side, no bridge needed).
203
+ let logEntries = []
204
+ try { logEntries = Array.isArray(activities()) ? activities() : [] } catch { /* keep empty */ }
205
+ await writeFile('log.json', JSON.stringify({ entries: logEntries.slice(0, 200) }, null, 2) + '\n')
206
+
207
+ // 3) meta + README (last two writes; meta.files covers the whole bundle).
208
+ const bridgeInfo = project.info ?? null
209
+ const allFileNames = [...files.map((f) => f.name), 'meta.json', 'README.txt']
210
+ const meta = {
211
+ kind: 'dsh-eda-snapshot',
212
+ createdAt: now().toISOString(),
213
+ project: project.info ?? null,
214
+ document: project.doc ?? null,
215
+ connected,
216
+ files: allFileNames,
217
+ errors,
218
+ }
219
+ await writeFile('meta.json', JSON.stringify(meta, null, 2) + '\n')
220
+
221
+ const readme = [
222
+ `# 紧急保存快照 — ${sanitizeName(bridgeInfo && typeof bridgeInfo === 'object' ? (bridgeInfo.name ?? '') : '') || 'EDA 快照'}`,
223
+ '',
224
+ `生成时间: ${meta.createdAt} 来源: dsh-lichuang-eda(官方桥 ${connected ? '已连接' : '未连接'})`,
225
+ '',
226
+ '## 打开方法(pro 与普通都兼容)',
227
+ `1. **专业版**(嘉立创EDA专业版 / pro.lceda.cn):文件 → 打开(或把文件拖入编辑器)选择 ${(project.docFile?.name ?? '.epro2 文件')} 即可完整恢复 agent 的全部改动。`,
228
+ `2. **普通/标准版**(EasyEDA 网页标准版):无法直接打开 .epro2;请双击 ${(project.preview?.name ?? 'preview.svg')} 用浏览器查看(通用预览),或把网表/BOM 数据交给 AI 用离线生成器重建(仅支持简单电路)。`,
229
+ '',
230
+ '## 文件清单',
231
+ ...meta.files.map((f) => `- ${f}`),
232
+ '',
233
+ '## 说明',
234
+ '- .epro2 为专业版原生格式(官方 API 仅能导出 epro/epro2 与 PDF/PNG/SVG,无标准版 JSON 通道),因此"pro 完整恢复 + 通用预览 + 数据文件"是官方能力下的最佳组合。',
235
+ '- log.json 记录 agent 每次官方 API 调用(放元件/连线/DRC/网表…),画板断连也能保留步骤。',
236
+ ...(errors.length > 0 ? ['', '## 本次降级项', ...errors.map((e) => `- ${e}`)] : []),
237
+ '',
238
+ ].join('\n')
239
+ await writeFile('README.txt', readme)
240
+
241
+ return {
242
+ ok: true,
243
+ dir: outDir,
244
+ files,
245
+ errors,
246
+ connected,
247
+ project: project.info,
248
+ document: project.doc,
249
+ docFile: project.docFile?.name ?? null,
250
+ preview: project.preview?.name ?? null,
251
+ }
252
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@windypro-rourou/dsh-eda",
3
+ "description": "嘉立创 EDA 助手(dsh-lichuang-eda)——面向 0 基础开发者,在 DSH Web GUI 里自动生成嘉立创 EDA(专业版/标准版)原理图/PCB 草图的插件。当前为可加载的骨架版本:DSH 插件表面(提供服务、HTTP 路由、agent 工具、系统提示通告、浏览器面板)已接通,EDA 后端尚未接入(backend: not-wired)。Hot-pluggable — mounted via profile cordis.patch.yml + profile node_modules copy, no dsh source changes.",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "packageManager": "pnpm@11.22.0",
7
+ "engines": {
8
+ "node": "^22.19.0 || >=24.0.0"
9
+ },
10
+ "main": "lib/index.js",
11
+ "exports": {
12
+ ".": {
13
+ "default": "./lib/index.js"
14
+ },
15
+ "./client": {
16
+ "default": "./lib/client.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "dsh": {
21
+ "bundle": {
22
+ "patch": "./cordis.patch.yml"
23
+ },
24
+ "client": {
25
+ "inject": [
26
+ "@deepseek-ai/dsh-client-runtime"
27
+ ],
28
+ "platform": "web"
29
+ }
30
+ },
31
+ "peerDependencies": {
32
+ "react": "^18.2.0",
33
+ "react-dom": "^18.2.0"
34
+ },
35
+ "files": [
36
+ "lib/**/*.js",
37
+ "src/**/*.js",
38
+ "cordis.patch.yml",
39
+ "README.md"
40
+ ],
41
+ "license": "Apache-2.0",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/WindyPro-rourou/JCL-EDA.git"
45
+ }
46
+ }