dsh-eda-mcp 0.7.0 → 0.7.2
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/CHANGELOG.md +90 -0
- package/CONTRIBUTING.md +0 -1
- package/README.md +13 -6
- package/README.zh.md +13 -6
- package/docs/release.md +1 -1
- package/docs/schematic-workflow.md +152 -0
- package/docs/tools.md +13 -9
- package/dsh-eda-bridge-extension-0.4.0.zip +0 -0
- package/jlc-extension/api-bridge.js +1 -1
- package/lib/bridge.mjs +440 -133
- package/lib/client.js +17 -1
- package/lib/index.js +278 -8
- package/package.json +3 -4
- package/src/bridge/index.ts +436 -112
- package/src/bridge/workflow-guide.ts +45 -0
- package/src/client/EdaMcpSettingsCard.tsx +10 -1
- package/src/client/locales.ts +4 -0
- package/src/guidance.ts +4 -0
- package/src/index.ts +45 -2
- package/src/routes.ts +3 -1
- package/src/skills-content.ts +164 -0
- package/src/skills.ts +91 -0
- package/test/skills.test.ts +62 -0
- package/dsh-eda-bridge-extension.zip +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schematic working standard, in one place: the short form goes into the DSH system
|
|
3
|
+
* prompt (guidance.ts) and the long form is served by the `eda_workflow_guide` tool,
|
|
4
|
+
* so the agent can pull the full playbook without bloating every session prompt.
|
|
5
|
+
*
|
|
6
|
+
* DSH has no plugin-facing skill-registration API, so a prompt section plus this
|
|
7
|
+
* on-demand document is the supported way to register a skill with a plugin.
|
|
8
|
+
*/
|
|
9
|
+
export const WORKFLOW_SUMMARY = [
|
|
10
|
+
'原理图工作规范(默认全部手工,禁止图省事用自动布局/自动布线):',
|
|
11
|
+
'1) 布局:按 10 单位栅格对齐;信号从左到右、电源在上、地在下;同级器件等间距;不压字、不压标题栏;先看 eda_get_page_info 的安全区再摆位。',
|
|
12
|
+
'2) 连线:只用 eda_draw_wire 走直线/45 度;**跨区域同名网络必须用网络端口(eda_place_net_port)或网络标签**,不能靠在两处画一条叫同名的导线——导线一旦接触引脚,net 会被连通性重算,指定名会被覆盖。',
|
|
13
|
+
'3) 网络标签:先试 eda_place_net_label;若该构建的 createNetLabel 不响应,会自动降级为纯文本标签(无电气语义),此时靠 stub 导线承载网络;批量用 eda_place_net_labels 一次调用,不要串行上百次。',
|
|
14
|
+
'4) 校验:eda_export_netlist 看每个网络的端点是否 ≥2;eda_run_drc 看逐条 DRC 明细;视觉检查优先导出 PNG 自行识别,而不是让 EDA 重排。',
|
|
15
|
+
'5) 只有用户明确要求时才 eda_auto_layout / eda_auto_route(需 force:true 且设置里放行)。',
|
|
16
|
+
'6) 引脚信息用 eda_get_pins 一次拿全(含 pinName/pinNumber),不要逐脚调 raw API。',
|
|
17
|
+
].join('\n')
|
|
18
|
+
|
|
19
|
+
export const WORKFLOW_GUIDE = WORKFLOW_SUMMARY + '\n\n' + [
|
|
20
|
+
'## 布局细则是怎么落地的',
|
|
21
|
+
'',
|
|
22
|
+
'- 坐标单位是 0.01 英寸(MCP 单位);A4 图框 0..1170 / 0..825,标题栏占 right 460..1160 / bottom 10..190,安全区见 eda_get_page_info。',
|
|
23
|
+
'- 一页只放一个功能块;器件按信号流从左到右排,反馈元件放在放大器下方,去耦电容贴近电源脚。',
|
|
24
|
+
'- 器件之间留出至少 20 单位用于走线和标签;网络端口放在模块边缘,左右各一列。',
|
|
25
|
+
'- 摆好一个模块再摆下一个;每次摆完用 eda_get_components 检查没有重叠(包围盒相交)。',
|
|
26
|
+
'',
|
|
27
|
+
'## 网络与标签',
|
|
28
|
+
'',
|
|
29
|
+
'- eda_place_net_port 产生真正的网络端口图元,是跨区域连接的首选。',
|
|
30
|
+
'- eda_place_net_label 优先调 sch_PrimitiveAttribute.createNetLabel;该 API 在 3.2.186 上不响应,探测结论按会话缓存,之后直接降级为文本标签(不再每次付 4 秒超时)。',
|
|
31
|
+
'- 降级后的文本标签没有电气语义:连通性由 eda_place_net_label_at_pin 同时画的 stub 导线承担。',
|
|
32
|
+
'- eda_get_net_labels 会同时列出属性图元(netlabel/netport/netflag)和文本标签,删除用 eda_delete_primitive 的对应 kind。',
|
|
33
|
+
'- 批量放置用 eda_place_net_labels([{pinPrimitiveId, net}, ...]),一次调用完成并逐条回报成败。',
|
|
34
|
+
'',
|
|
35
|
+
'## 导线 net 被覆盖(务必知道)',
|
|
36
|
+
'',
|
|
37
|
+
'eda_draw_wire 传的 net 只对**悬空导线**有效;导线端点一旦落在引脚/网络上,EDA 会按连通性重算网络名(实测 TESTNET 并入了 LED_A)。所以“同名导线 = 同名网络”不成立,跨区域同名必须用网络标签或网络端口。',
|
|
38
|
+
'',
|
|
39
|
+
'## 校验顺序',
|
|
40
|
+
'',
|
|
41
|
+
'1. eda_get_active_document 确认在原理图页(不是 HOME/PCB)。',
|
|
42
|
+
'2. eda_export_netlist(已解析成对象)检查每个网络 ≥2 端点、无悬空。',
|
|
43
|
+
'3. eda_run_drc 看逐条明细(含 primitiveId 与坐标)。',
|
|
44
|
+
'4. 需要人看效果就导出图片自行识别,不要用自动布局“修”版面。',
|
|
45
|
+
].join('\n')
|
|
@@ -11,6 +11,7 @@ export interface EdaMcpSettings {
|
|
|
11
11
|
toolCallTimeoutMs?: number
|
|
12
12
|
allowRawApi?: boolean
|
|
13
13
|
edaExecutable?: string
|
|
14
|
+
allowAutoLayoutRouting?: boolean
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface EdaMcpSettingsCardState {
|
|
@@ -450,13 +451,21 @@ export function EdaMcpSettingsCard(props: EdaMcpSettingsCardProps) {
|
|
|
450
451
|
disabled={disabled}
|
|
451
452
|
onChange={(value) => { clearNotice(); update('allowRawApi', value) }}
|
|
452
453
|
/>
|
|
454
|
+
<BooleanField
|
|
455
|
+
label={t('settings.allowAutoLayoutRouting')}
|
|
456
|
+
hint={t('settings.allowAutoLayoutRoutingHint')}
|
|
457
|
+
value={draft.allowAutoLayoutRouting ?? false}
|
|
458
|
+
disabled={disabled}
|
|
459
|
+
onChange={(value) => { clearNotice(); update('allowAutoLayoutRouting', value) }}
|
|
460
|
+
/>
|
|
453
461
|
|
|
454
462
|
<p style={{ ...hintStyle, margin: '8px 0 0' }}>{t('settings.portLabel')}</p>
|
|
455
463
|
|
|
456
464
|
<Field label={t('settings.downloadExtension')} hint={t('settings.installHint')}>
|
|
457
465
|
<a
|
|
458
466
|
href="/api/dsh-eda-mcp/extension.zip"
|
|
459
|
-
download
|
|
467
|
+
// No download name: the server sends the versioned file name, so the
|
|
468
|
+
// saved copy always states which build it is.
|
|
460
469
|
style={{ ...secondaryButtonStyle, textDecoration: 'none', display: 'inline-flex', width: 'fit-content' }}
|
|
461
470
|
>
|
|
462
471
|
{t('settings.downloadExtension')}
|
package/src/client/locales.ts
CHANGED
|
@@ -9,6 +9,8 @@ export const zh = {
|
|
|
9
9
|
'settings.toolCallTimeoutMsHint': '单次 EDA API 调用超时,默认 60000。',
|
|
10
10
|
'settings.allowRawApi': '允许通用 eda_api_call',
|
|
11
11
|
'settings.allowRawApiHint': '开启后 MCP 工具 eda_api_call 可调用白名单内的 JLCEDA 扩展 API。',
|
|
12
|
+
'settings.allowAutoLayoutRouting': '允许原理图自动布局 / 自动布线',
|
|
13
|
+
'settings.allowAutoLayoutRoutingHint': '默认关闭:布局按栅格手工摆放、布线手工走,版面检查用导出图片自行识别。开启后 eda_auto_layout / eda_auto_route 才可用。',
|
|
12
14
|
'settings.edaExecutable': '嘉立创EDA 可执行文件(可选)',
|
|
13
15
|
'settings.edaExecutableHint': '留空即自动探测:环境变量 → 注册表登记的文件关联 → 常见安装目录 → PATH。只有自动探测失败时才需要填完整路径。',
|
|
14
16
|
'settings.save': '保存',
|
|
@@ -41,6 +43,8 @@ export const en = {
|
|
|
41
43
|
'settings.allowRawApi': 'Allow generic eda_api_call',
|
|
42
44
|
'settings.allowRawApiHint': 'Expose the eda_api_call tool restricted to the allow-listed JLCEDA APIs.',
|
|
43
45
|
'settings.edaExecutable': 'JLCEDA Pro executable (optional)',
|
|
46
|
+
'settings.allowAutoLayoutRouting': 'Allow automatic schematic layout / routing',
|
|
47
|
+
'settings.allowAutoLayoutRoutingHint': 'Off by default: place parts on the grid, route by hand, and check the sheet by exporting an image. Only with this on do eda_auto_layout / eda_auto_route run.',
|
|
44
48
|
'settings.edaExecutableHint': 'Leave empty to auto-detect: environment variable, the file-association command in the registry, conventional install directories, then PATH. Set a full path only when auto-detection fails.',
|
|
45
49
|
'settings.save': 'Save',
|
|
46
50
|
'settings.saving': 'Saving…',
|
package/src/guidance.ts
CHANGED
|
@@ -21,5 +21,9 @@ export const EDA_MCP_GUIDANCE = [
|
|
|
21
21
|
'绘制细节:原理图坐标为 0.01inch,原点在 A4 图框左下角,y 向上,图框 x 0~1170、y 0~825,标题栏 x 460~1160、y 10~190 必须避让(先 eda_get_page_info)。PCB 坐标为 mil。板框用 pcb_draw_board_outline(板框层 11 的一条 pcb_PrimitivePolyline,线宽默认 10mil);区域只能用铜层 top/bottom/内层(15–44)/MULTI(12)。',
|
|
22
22
|
'长任务:eda_pcb_auto_route 默认异步启动(EDA 自己显示进度条),必须用 eda_pcb_job_status 轮询 running 直到 false,不要加大超时;eda_pcb_clear_routing 同理(旧扩展自动分批,按 done 继续调用)。',
|
|
23
23
|
'原理图 API(sch_Net.*、网表导出)只作用于当前活动文档:返回空时先用 eda_get_active_document 查、eda_open_document 切到原理图页。',
|
|
24
|
+
'原理图工作规范(默认手工,禁止为省事用自动布局/自动布线):按 10 单位栅格对齐;信号左→右、电源在上、地在下;器件间留 ≥20 单位走线与标签空间;先 eda_get_page_info 看安全区再摆位;摆完用 eda_get_components 检查包围盒不重叠。eda_auto_layout / eda_auto_route 默认被拒绝(需 force:true 且设置里放行),仅当用户明确要求时使用;版面检查优先"导出图片自行识别"而不是让 EDA 重排。',
|
|
25
|
+
'跨区域同名网络必须用 eda_place_net_port / 网络标签:eda_draw_wire 的 net 只对悬空导线有效,导线一旦接触引脚,EDA 会按连通性重算网络名(实测 TESTNET 被并入 LED_A),所以"同名导线 = 同名网络"不成立。',
|
|
26
|
+
'网络标签:eda_place_net_label 先真实调 createNetLabel,该 API 在 3.2.186 上不响应 → 探测结论按会话缓存后自动降级为纯文本标签(无电气语义,靠 stub 导线承载网络);批量务必用 eda_place_net_labels 一次调用,不要串行上百次(会打爆 run_code 超时且没有进度)。标签可枚举/清理:eda_get_net_labels + eda_delete_primitive(kind: netlabel|netport|netflag|text)。',
|
|
27
|
+
'引脚身份用 eda_get_pins 一次拿全(含 pinName/pinNumber),不要逐脚调 raw sch_PrimitivePin.get;原理图挪器件用 eda_move_component(保 designator/value),不要删了重放。完整工作手册:调 eda_workflow_guide。',
|
|
24
28
|
'连接问题先调 eda_status:connectedClients=0 表示 EDA 未连接(确认 EDA 已打开、扩展已导入并开启「允许外部交互」、端口 39009 空闲);extensionVersion 与 bridgeVersion 不一致或报 API method not allowed 表示扩展是旧版,需要重新导入 dsh-eda-bridge-extension.zip 并重启 EDA。',
|
|
25
29
|
].join('\n')
|
package/src/index.ts
CHANGED
|
@@ -5,8 +5,10 @@ import type {} from '@deepseek-ai/dsh-settings'
|
|
|
5
5
|
import * as mcpClient from '@deepseek-ai/dsh-mcp-client'
|
|
6
6
|
import z from '@deepseek-ai/schemastery'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
8
9
|
import { dirname } from 'node:path'
|
|
9
10
|
import { EDA_MCP_GUIDANCE, SECTION_ORDER } from './guidance.ts'
|
|
11
|
+
import { createEdaSkillProvider } from './skills.ts'
|
|
10
12
|
import { makeEdaMcpRoutes, makeEdaMcpSettingsRoutes, type SettingsBridge } from './routes.ts'
|
|
11
13
|
import { resolveBridgeRuntime } from './node-runtime.ts'
|
|
12
14
|
import { BridgeDiagnostics } from './bridge-diagnostics.ts'
|
|
@@ -22,6 +24,8 @@ export interface EdaMcpConfig {
|
|
|
22
24
|
allowRawApi?: boolean
|
|
23
25
|
/** Path to lceda-pro.exe; empty lets eda_launch discover it. */
|
|
24
26
|
edaExecutable?: string
|
|
27
|
+
/** Allow eda_auto_layout / eda_auto_route (off by default: place and route by hand). */
|
|
28
|
+
allowAutoLayoutRouting?: boolean
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -37,6 +41,11 @@ export const Config = z.object({
|
|
|
37
41
|
allowRawApi: z.boolean().default(true).volatile(),
|
|
38
42
|
/** Optional path to lceda-pro.exe; empty means "let eda_launch discover it". */
|
|
39
43
|
edaExecutable: z.string().default('').volatile(),
|
|
44
|
+
/**
|
|
45
|
+
* Automatic schematic layout/routing stays off unless enabled here: the default
|
|
46
|
+
* working style is grid-aligned manual placement plus a visual check.
|
|
47
|
+
*/
|
|
48
|
+
allowAutoLayoutRouting: z.boolean().default(false).volatile(),
|
|
40
49
|
})
|
|
41
50
|
|
|
42
51
|
/** Hooks a settings registration hands to the DSH settings seam. */
|
|
@@ -141,7 +150,27 @@ function installSettingsSection<T>(ctx: Context, scoped: Context, ns: string, sc
|
|
|
141
150
|
export const inject = ['webServer', 'systemPrompt']
|
|
142
151
|
|
|
143
152
|
const BRIDGE_URL = new URL('../lib/bridge.mjs', import.meta.url)
|
|
144
|
-
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The extension zip is shipped as `dsh-eda-bridge-extension-<version>.zip` (the
|
|
156
|
+
* version is the wire contract in jlc-extension/extension.json). Resolve it from
|
|
157
|
+
* that manifest instead of hard-coding a file name; older layouts without a
|
|
158
|
+
* versioned file keep working through the plain-name fallback.
|
|
159
|
+
*/
|
|
160
|
+
function resolveExtensionZipPath(): string {
|
|
161
|
+
const plain = fileURLToPath(new URL('../dsh-eda-bridge-extension.zip', import.meta.url))
|
|
162
|
+
try {
|
|
163
|
+
const manifest = JSON.parse(readFileSync(fileURLToPath(new URL('../jlc-extension/extension.json', import.meta.url)), 'utf8')) as { version?: string }
|
|
164
|
+
const version = typeof manifest.version === 'string' ? manifest.version : ''
|
|
165
|
+
if (version !== '') {
|
|
166
|
+
const versioned = fileURLToPath(new URL('../dsh-eda-bridge-extension-' + version + '.zip', import.meta.url))
|
|
167
|
+
if (existsSync(versioned)) return versioned
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Bundle-only installs may lack the manifest; the plain name is the fallback.
|
|
171
|
+
}
|
|
172
|
+
return plain
|
|
173
|
+
}
|
|
145
174
|
|
|
146
175
|
/** Bridge script path, resolved once. */
|
|
147
176
|
const BRIDGE_PATH = fileURLToPath(BRIDGE_URL)
|
|
@@ -161,6 +190,7 @@ function bridgeLaunchFor(active: EdaMcpConfig) {
|
|
|
161
190
|
// back to the schema default otherwise, so a stale wrapper can never produce a
|
|
162
191
|
// client config that fails validation.
|
|
163
192
|
const allowRawApi = booleanField(active.allowRawApi, true)
|
|
193
|
+
const allowAutoLayoutRouting = booleanField(active.allowAutoLayoutRouting, false)
|
|
164
194
|
const toolCallTimeoutMs = numberField(active.toolCallTimeoutMs, 60000)
|
|
165
195
|
return {
|
|
166
196
|
transport: 'stdio' as const,
|
|
@@ -170,6 +200,7 @@ function bridgeLaunchFor(active: EdaMcpConfig) {
|
|
|
170
200
|
env: {
|
|
171
201
|
...runtime.extraEnv,
|
|
172
202
|
DSH_EDA_MCP_ALLOW_RAW_API: allowRawApi ? '1' : '0',
|
|
203
|
+
DSH_EDA_ALLOW_AUTO_LAYOUT: allowAutoLayoutRouting ? '1' : '0',
|
|
173
204
|
DSH_EDA_MCP_TOOL_TIMEOUT_MS: String(toolCallTimeoutMs),
|
|
174
205
|
// eda_launch falls back to the registry / install dirs / PATH when empty.
|
|
175
206
|
DSH_EDA_EXECUTABLE: typeof active.edaExecutable === 'string' ? active.edaExecutable : '',
|
|
@@ -252,7 +283,7 @@ export function apply(ctx: Context, config?: EdaMcpConfig): void {
|
|
|
252
283
|
ctx.effect(() => {
|
|
253
284
|
const disposers = makeEdaMcpRoutes({
|
|
254
285
|
getConfig: () => current(),
|
|
255
|
-
extensionZipPath:
|
|
286
|
+
extensionZipPath: resolveExtensionZipPath(),
|
|
256
287
|
reconnect: remountClient,
|
|
257
288
|
bridgeLaunch: describeBridgeLaunch,
|
|
258
289
|
diagnostics: () => diagnostics.liveSnapshot(),
|
|
@@ -272,6 +303,18 @@ export function apply(ctx: Context, config?: EdaMcpConfig): void {
|
|
|
272
303
|
}, 'eda-mcp: settings bridge')
|
|
273
304
|
})
|
|
274
305
|
|
|
306
|
+
// Register the schematic standard as a DSH skill, so it lands in the session skill
|
|
307
|
+
// catalog next to local/project skills and loads on demand with the `skill` tool.
|
|
308
|
+
// Structural typing + a runtime guard keeps older hosts (no skills service) working.
|
|
309
|
+
ctx.inject(['skills'], (sctx) => {
|
|
310
|
+
const register = (sctx as unknown as {
|
|
311
|
+
skills?: { registerProvider?: (factory: (control: unknown) => unknown) => () => void }
|
|
312
|
+
}).skills?.registerProvider
|
|
313
|
+
if (typeof register !== 'function') return
|
|
314
|
+
const dispose = register(() => createEdaSkillProvider())
|
|
315
|
+
return () => { dispose() }
|
|
316
|
+
})
|
|
317
|
+
|
|
275
318
|
const sync = (): void => {
|
|
276
319
|
if (disposeSection !== undefined) {
|
|
277
320
|
disposeSection()
|
package/src/routes.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs'
|
|
2
|
+
import { basename } from 'node:path'
|
|
2
3
|
import type { ServerResponse } from 'node:http'
|
|
3
4
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
4
5
|
import { SettingsConflictError, type SettingsPathOp } from '@deepseek-ai/dsh-settings'
|
|
@@ -186,7 +187,8 @@ export function makeEdaMcpRoutes(deps: EdaMcpRouteDeps): WebRoute[] {
|
|
|
186
187
|
if (!isLoopbackRequest(req)) return json(res, 403, { ok: false, error: 'forbidden' })
|
|
187
188
|
res.writeHead(200, {
|
|
188
189
|
'content-type': 'application/zip',
|
|
189
|
-
|
|
190
|
+
// Keep the versioned file name the browser saves it as.
|
|
191
|
+
'content-disposition': 'attachment; filename="' + basename(deps.extensionZipPath) + '"',
|
|
190
192
|
'cache-control': 'no-store',
|
|
191
193
|
})
|
|
192
194
|
const stream = createReadStream(deps.extensionZipPath)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The complete JLCEDA Pro project playbook served as the DSH skill body.
|
|
3
|
+
*
|
|
4
|
+
* Single source for the skill text, the generated docs/schematic-workflow.md and the
|
|
5
|
+
* condensed system-prompt section (src/bridge/workflow-guide.ts) — keep them in step.
|
|
6
|
+
*
|
|
7
|
+
* Every entry is one physical line of markdown; long paragraphs are pre-wrapped so
|
|
8
|
+
* the emitted module never needs inside-string escapes.
|
|
9
|
+
*/
|
|
10
|
+
export const SKILL_PLAYBOOK_LINES: readonly string[] = [
|
|
11
|
+
"# 嘉立创EDA 项目全流程手册(dsh-eda-mcp)",
|
|
12
|
+
"",
|
|
13
|
+
"覆盖「打开/新建工程 → 原理图 → 同步到 PCB → 布局布线 → 校验 → 出 Gerber」全链路,",
|
|
14
|
+
"以及每一步已经踩过的坑与处置办法。工具前缀统一是 mcp__jlceda__。",
|
|
15
|
+
"",
|
|
16
|
+
"## 0. 适用范围与硬前提",
|
|
17
|
+
"",
|
|
18
|
+
"- 原理图坐标单位 0.01 英寸,原点在 A4 图框左下角,y 向上;图框 x 0~1170 / y 0~825。",
|
|
19
|
+
"- 标题栏占 x 460~1160 / y 10~190,必须避让;先 eda_get_page_info 读图框与安全区。PCB 坐标单位 mil。",
|
|
20
|
+
"- 绝大多数原理图 API 只作用于「当前活动文档」:返回空先 eda_get_active_document,再 eda_open_document 切页。",
|
|
21
|
+
"- 线协议版本必须桥接与扩展一致:eda_status 的 extensionVersions 是每个页面连上来的版本;",
|
|
22
|
+
" 不一致说明装的是旧扩展,需重新导入 dsh-eda-bridge-extension-<版本>.zip 并重启应用(不是 reload)。",
|
|
23
|
+
"- 默认禁止 eda_auto_layout / eda_auto_route(需 force:true 且设置里放行)。",
|
|
24
|
+
" 版面检查用「导出图片自行识别」,不要让 EDA 自动重排。",
|
|
25
|
+
"",
|
|
26
|
+
"## 1. 项目层:列出 / 打开 / 新建 / 保存 / 导出",
|
|
27
|
+
"",
|
|
28
|
+
"- 列工程:eda_list_projects。无过滤时遍历所有本地目录聚合,字段 friendlyName / teamUuid / uuid。",
|
|
29
|
+
" 半离线模式下每个本地工程目录就是一个 team,其 uuid 就是目录路径。",
|
|
30
|
+
"- 打开工程:eda_open_project({ name }) 支持中文工程名(精确优先,其次忽略大小写包含)。",
|
|
31
|
+
" 返回的 opened 是「已观察到的活动工程」,另有 openReturned 反映 API 原始布尔。",
|
|
32
|
+
"- 新建工程:createProject 在本机构建上无副作用,**用 eda_import_project 导入工程文件代替**:",
|
|
33
|
+
" eda_import_project({ path, teamUuid, friendlyName, open:true }),teamUuid 取 eda_list_teams 的目录路径。",
|
|
34
|
+
"- 导出:eda_export_project({ outPath }) 落盘并返回字节数;不给 outPath 只回元数据,inline:true 才回 base64。",
|
|
35
|
+
"- 保存:eda_save_document(原理图)/ eda_pcb_save_document(PCB)。关应用前先保存。",
|
|
36
|
+
"- 不可用(实测):createProject / deleteProject / copyProject 对本地工程无效;",
|
|
37
|
+
" 删除与复制请在应用内完成,复制也可「导出 + 导入」得到独立副本。",
|
|
38
|
+
"",
|
|
39
|
+
"## 2. 原理图:先读清楚再动手",
|
|
40
|
+
"",
|
|
41
|
+
"- eda_get_active_document → 确认 documentTypeName 是原理图页(HOME / PCB 都不行)。",
|
|
42
|
+
"- eda_get_page_info → 图框、标题栏、安全区(main / lowerLeft)。",
|
|
43
|
+
"- eda_get_components({ includeBounds:true }) → designator、value、x/y、包围盒,用来判断重叠。",
|
|
44
|
+
"- eda_get_pins({ componentPrimitiveId }) → 一次拿到 pinName / pinNumber / x / y / rotation / noConnected,",
|
|
45
|
+
" 不要逐脚调 raw sch_PrimitivePin.get。",
|
|
46
|
+
"- eda_get_wires / eda_get_nets → 现有连线与网络;eda_get_net_labels → 标签、网络端口、旗标与文本标签。",
|
|
47
|
+
"- eda_get_project → 工程结构(board → schematic / pcb 的 uuid),后面切页与 PCB 同步都要用。",
|
|
48
|
+
"",
|
|
49
|
+
"## 3. 原理图:布局规范",
|
|
50
|
+
"",
|
|
51
|
+
"- 栅格 10 单位对齐;同块内器件等间距(建议 ≥30 单位),块间留 ≥40 单位。",
|
|
52
|
+
"- 信号流左→右;电源旗标朝上、地旗标朝下;主控居中,去耦电容贴电源脚(≤20 单位),晶振贴芯片且周围净空。",
|
|
53
|
+
"- 器件之间留 ≥20 单位用于走线与标签,避免标签压住符号或引脚号。",
|
|
54
|
+
"- 分区用 eda_draw_functional_box(虚线框 + 文本标注):电源 / 主控 / 接口 / 信号处理。",
|
|
55
|
+
"- 挪已有器件用 eda_move_component(保 designator/value),不要「删除 + 重放」。",
|
|
56
|
+
"- 摆完一屏回读 eda_get_components 检查包围盒相交,有重叠先调开再连线。",
|
|
57
|
+
"- 版面自查:导出画布图片交给模型识别,而不是让 EDA 自动重排(自动布局会把分好的区打散)。",
|
|
58
|
+
"",
|
|
59
|
+
"## 4. 原理图:连线与网络",
|
|
60
|
+
"",
|
|
61
|
+
"- 局部连接用 eda_draw_wire(直线或 45°),不要穿过器件符号,不要与其它导线重叠。",
|
|
62
|
+
"- **跨区域同名网络必须用 eda_place_net_port 或网络标签**:eda_draw_wire 的 net 只对悬空导线有效,",
|
|
63
|
+
" 端点一接触引脚,EDA 按连通性重算网络名(实测 TESTNET 被并入 LED_A)。「两处画同名导线」不成立。",
|
|
64
|
+
"- 网络标签:eda_place_net_label 先真实调 sch_PrimitiveAttribute.createNetLabel;该 API 在 3.2.186 上不响应,",
|
|
65
|
+
" 插件按会话缓存探测结论后自动降级为纯文本标签(无电气语义),连通性由 stub 导线承担。",
|
|
66
|
+
"- 引脚上放标签/旗标优先用 *_at_pin 变体:沿引脚方向外推 stubLength(默认 20)再放,避免压住符号。",
|
|
67
|
+
"- 差分对写成 USB_DP / USB_DM 两个网络,不要写 USB_DP/DM;网络名只允许字母数字下划线点加减号。",
|
|
68
|
+
"- 不用的引脚打未连接标记:eda_set_no_connect({ primitiveIds })。",
|
|
69
|
+
"- 删除图元:eda_delete_primitive 的 kind 支持 component / wire / text / netport / netflag / netlabel;",
|
|
70
|
+
" 网络端口不出现在 eda_get_components 中,只能用 eda_get_net_labels 审计后再删。",
|
|
71
|
+
"",
|
|
72
|
+
"## 5. 原理图:批量操作与超时控制",
|
|
73
|
+
"",
|
|
74
|
+
"- 批量放标签用 eda_place_net_labels([{ pinPrimitiveId, net } | { x, y, net }]):一次调用、逐条 ok/error、",
|
|
75
|
+
" 返回 done / failed / total。不要串行上百次(会打爆调用超时且没有进度可查)。",
|
|
76
|
+
"- 批量未连接标记用 eda_set_no_connect({ primitiveIds }),内部并发 8。",
|
|
77
|
+
"- 单次调用建议 ≤20 个变更,中间夹一次读取(eda_get_net_labels / eda_get_components)作为进度锚点;",
|
|
78
|
+
" 超时后凭锚点知道完成到哪,而不是全量重来。",
|
|
79
|
+
"- 探测类失败只付一次代价:net label 能力探测结果按会话缓存,probe:true 才重测。",
|
|
80
|
+
"",
|
|
81
|
+
"## 6. 原理图:校验与交付",
|
|
82
|
+
"",
|
|
83
|
+
"- eda_export_netlist → 已解析对象(parsed:true, netlist)。逐网络检查端点数 ≥2、无悬空单脚网络。",
|
|
84
|
+
"- eda_run_drc → 逐条明细(type / message / primitiveId / x / y),按 primitiveId 回画布定位后修。",
|
|
85
|
+
"- eda_export_bom(csv 已自动从 UTF-16 解码)→ 位号连续、封装非空、无 DUMMY。",
|
|
86
|
+
"- 交付前把网络名清单与需求表比对(命名一致性比 DRC 更容易漏)。",
|
|
87
|
+
"",
|
|
88
|
+
"## 7. PCB:从同步到出 Gerber",
|
|
89
|
+
"",
|
|
90
|
+
"- 同步:eda_open_document 切到 PCB 页 → pcb_import_changes(默认异步,会弹应用自己的确认对话框)→",
|
|
91
|
+
" eda_pcb_wait_for_components 判定「新器件出现了」;不要用长超时硬等对话框。",
|
|
92
|
+
"- 读现状:pcb_get_layers、pcb_get_nets({detail:true})、pcb_get_components({includePins:true})。",
|
|
93
|
+
"- 板框:pcb_draw_board_outline({ points }) 生成 layer 11 的闭合 polyline。",
|
|
94
|
+
" 不要用 pcb_draw_track 画板框(铜线,layer 11 被拒),也不要用 region(只支持铜层/MULTI)。",
|
|
95
|
+
"- 布局:pcb_place_component / pcb_move_component 手工摆放(安装孔 → 板边接口 → 主控 → 配套器件);",
|
|
96
|
+
" 去耦电容距引脚 ≤2mm,晶振靠芯片且下方净空。",
|
|
97
|
+
"- 走线:pcb_draw_track(不传线宽按网络角色给默认值,低于 6mil 直接拒绝);改线用 pcb_modify_track;",
|
|
98
|
+
" 整网加粗用 pcb_set_net_track_width。",
|
|
99
|
+
"- 过孔:pcb_draw_via(默认孔径按角色给标准值;焊环 <0.2mm 拒绝,孔径 <0.3mm 警告)。",
|
|
100
|
+
"- 铺铜:pcb_create_pour(points 为 mil 扁平数组,自动闭合);先确认设计规则里铺铜间距 ≥6mil(建议 10mil)。",
|
|
101
|
+
"- 自动布线:eda_pcb_auto_route 默认不用;确需使用才 wait:true 或后台 + eda_pcb_job_status 轮询。",
|
|
102
|
+
"- 校验:eda_pcb_run_drc(includeVerboseError:true 拿逐条)+ eda_design_audit(线宽/过孔/网络名/板框/铺铜/DRC 只读体检)。",
|
|
103
|
+
"- 交付:eda_pcb_export_gerber(返回 zip 的 base64 data URL,大板可能几 MB)。",
|
|
104
|
+
"",
|
|
105
|
+
"## 8. 设计规则速查(工具已强制)",
|
|
106
|
+
"",
|
|
107
|
+
"- 网络名:字母/数字/下划线/点/加号/减号,≤255 字符;空格、斜杠、括号、中文会被拒绝。",
|
|
108
|
+
"- 线宽:普通信号 ≥6mil(0.15mm)、去耦分支 10mil、主干电源 16mil、>500mA 或 GND 20mil。",
|
|
109
|
+
"- 过孔:信号 0.2/0.4mm、电源/GND 0.3/0.6mm、大电流 0.4/0.8mm;焊环 ≥0.2mm 强制。",
|
|
110
|
+
"- 铺铜:顶/底层铺 GND,与其它网络间距 ≥6mil(建议 10mil),导出前移除孤岛。",
|
|
111
|
+
"- 丝印:字符高度/宽度有最小值限制(见 docs/design-rules.md),丝印不得压焊盘。",
|
|
112
|
+
"",
|
|
113
|
+
"## 9. 故障与处置(现象 → 原因 → 动作)",
|
|
114
|
+
"",
|
|
115
|
+
"| 现象 | 原因 | 动作 |",
|
|
116
|
+
"|---|---|---|",
|
|
117
|
+
"| connectedClients=0 / 报未连接 | EDA 没开、扩展没导入或没点「连接」、端口被占 | 先 eda_status 看 hint;必要时 eda_launch 起应用并等扩展连上 |",
|
|
118
|
+
"| extensionVersions 与 bridgeVersion 不一致 | 装的是旧扩展 | 重新导入对应版本 zip(文件名带版本号)并重启应用 |",
|
|
119
|
+
"| API method not allowed | 该 API 不在扩展白名单 | 调 eda_list_api_methods 看清单;优先用包装好的工具 |",
|
|
120
|
+
"| 调用 60s 超时 | 页面切走留下僵尸 socket,或单次改太多 | 只读调用会自动换页面重试一次;把批量拆到 ≤20/批 |",
|
|
121
|
+
"| 读图返回空 / null | 活动文档不是原理图页 | eda_get_active_document → eda_open_document 切页 |",
|
|
122
|
+
"| createNetLabel 无响应 | 3.2.186 该 API 不 resolve | 自动降级文本标签 + stub 导线;批量用 eda_place_net_labels |",
|
|
123
|
+
"| eda_get_net_labels 曾返回 0 | 以前只查属性图元 | 现在同时列文本标签(kind:text),可直接删 |",
|
|
124
|
+
"| 导线 net 被改掉 | 连通性重算覆盖指定名 | 跨区域改用网络端口 / 网络标签 |",
|
|
125
|
+
"| 器件位置重叠 | 没按栅格、没留间距 | eda_move_component 重排;摆完回读包围盒 |",
|
|
126
|
+
"| 导入 .epro2 后 uuid 冲突 | 应用原样复制内部 id | 看返回 collisions;避免同时打开源工程与新工程 |",
|
|
127
|
+
"| createProject / copyProject / deleteProject 空或 false | 该构建对本地工程不支持 | 用 eda_import_project 建;删除/复制在应用内做 |",
|
|
128
|
+
"| eda_launch 带 project 没打开工程 | 命令行路径被应用忽略 | 用 eda_open_project({ name }) 打开 |",
|
|
129
|
+
"| 关应用后改动丢失 | eda_close 走了强制路径 | 关前先保存;看返回的 forced |",
|
|
130
|
+
"| run_code 读大 JSON 被截断 | 宿主 read 有单行上限 | 用 node:fs 读盘或分片读 |",
|
|
131
|
+
"",
|
|
132
|
+
"## 10. 端到端剧本",
|
|
133
|
+
"",
|
|
134
|
+
"A. 在已有工程里改原理图:",
|
|
135
|
+
" eda_list_projects → eda_open_project({name}) → eda_get_project(取 schematic uuid)→ eda_open_document",
|
|
136
|
+
" → eda_get_page_info → eda_get_components / eda_get_pins → 摆位(eda_place_component / eda_move_component)",
|
|
137
|
+
" → 连线 eda_draw_wire + 跨区域 eda_place_net_labels → eda_export_netlist 查端点 → eda_run_drc 看明细",
|
|
138
|
+
" → eda_save_document。",
|
|
139
|
+
"",
|
|
140
|
+
"B. 新建工程并出板:",
|
|
141
|
+
" 准备空白/模板工程文件 → eda_import_project({ path, teamUuid, friendlyName, open:true })",
|
|
142
|
+
" → 原理图流程(同 A)→ eda_get_project 取 pcb uuid → eda_open_document(pcb) → pcb_import_changes",
|
|
143
|
+
" → eda_pcb_wait_for_components 等确认 → 板框/布局/走线/过孔/铺铜 → eda_pcb_run_drc + eda_design_audit",
|
|
144
|
+
" → eda_pcb_save_document → eda_pcb_export_gerber → eda_export_project 备份。",
|
|
145
|
+
"",
|
|
146
|
+
"C. 只做 PCB:",
|
|
147
|
+
" eda_open_project → eda_get_project → eda_open_document(pcb) → pcb_get_nets(detail) / pcb_get_components(includePins)",
|
|
148
|
+
" → 板框与布局 → 走线(按角色线宽)→ 铺铜 → eda_pcb_run_drc + eda_design_audit → Gerber。",
|
|
149
|
+
"",
|
|
150
|
+
"## 11. 交付前检查清单",
|
|
151
|
+
"",
|
|
152
|
+
"- [ ] eda_status:connectedClients ≥1 且版本一致。",
|
|
153
|
+
"- [ ] 网表:每个网络 ≥2 端点、无悬空;差分对成对;电源/地命名统一。",
|
|
154
|
+
"- [ ] DRC:原理图与 PCB 都跑过,明细逐条清零或已确认豁免。",
|
|
155
|
+
"- [ ] 设计规则:线宽/过孔/铺铜间距/丝印符合第 8 节。",
|
|
156
|
+
"- [ ] BOM:位号连续、封装非空、无 DUMMY。",
|
|
157
|
+
"- [ ] 视觉:导出图片自查,标签不压符号、器件不重叠、功能框清晰。",
|
|
158
|
+
"- [ ] 保存:文档已保存,工程已 eda_export_project 备份。",
|
|
159
|
+
"- [ ] Gerber 已导出且层数与板层一致。",
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
export const SKILL_SECTIONS: readonly string[] = SKILL_PLAYBOOK_LINES.filter(line => line.startsWith("## "))
|
|
163
|
+
|
|
164
|
+
export const SKILL_PLAYBOOK = SKILL_PLAYBOOK_LINES.join(String.fromCharCode(10))
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill registration: dsh-eda-mcp exposes its schematic working standard as a DSH
|
|
3
|
+
* skill, so it appears in the session skill catalog and the agent can load it on
|
|
4
|
+
* demand with the `skill` tool instead of carrying the whole playbook in every
|
|
5
|
+
* system prompt.
|
|
6
|
+
*
|
|
7
|
+
* DSH has no plugin-facing package to depend on here; the provider shape is
|
|
8
|
+
* structural (ctx.skills.registerProvider), which keeps this build working across
|
|
9
|
+
* DSH generations that ship the service under slightly different types.
|
|
10
|
+
*/
|
|
11
|
+
import { SKILL_PLAYBOOK, SKILL_PLAYBOOK_LINES } from './skills-content.ts'
|
|
12
|
+
import { WORKFLOW_GUIDE, WORKFLOW_SUMMARY } from './bridge/workflow-guide.ts'
|
|
13
|
+
|
|
14
|
+
export const EDA_SKILL_NAME = 'jlceda-schematic-workflow'
|
|
15
|
+
export const EDA_SKILL_PROVIDER = 'dsh-eda-mcp'
|
|
16
|
+
|
|
17
|
+
/** DSH ranks project skills 100, custom dirs 300, user root 400, bundled 600. */
|
|
18
|
+
const EDA_SKILL_RANK = 500
|
|
19
|
+
|
|
20
|
+
const DESCRIPTION =
|
|
21
|
+
'Draw or edit a JLCEDA Pro (EasyDraw Pro) schematic through the dsh-eda-mcp tools: grid-aligned manual placement, ' +
|
|
22
|
+
'signal-flow layout, net ports/labels instead of same-named wires, batch net-label placement, and netlist/DRC verification. ' +
|
|
23
|
+
'TRIGGER when: laying out or re-laying a schematic, connecting pins across a sheet, placing many net labels, ' +
|
|
24
|
+
'checking a schematic (netlist endpoints, DRC detail), or moving parts without losing designators. ' +
|
|
25
|
+
'DO NOT TRIGGER for PCB-only work (routing, copper pour, gerber) or for project/application control (launch, open, import, export).'
|
|
26
|
+
|
|
27
|
+
const WHEN_TO_USE =
|
|
28
|
+
'Use before touching a schematic sheet, and again whenever a layout or connectivity decision has to be made.'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The skill body: the full project playbook (project → schematic → PCB → rules →
|
|
32
|
+
* troubleshooting → end-to-end scripts → delivery checklist), with the condensed
|
|
33
|
+
* bridge guide appended so the skill and eda_workflow_guide never contradict each
|
|
34
|
+
* other on the quick rules.
|
|
35
|
+
*/
|
|
36
|
+
export function skillBody(): string {
|
|
37
|
+
return [
|
|
38
|
+
SKILL_PLAYBOOK,
|
|
39
|
+
'',
|
|
40
|
+
'---',
|
|
41
|
+
'',
|
|
42
|
+
'## 附:精简版规范(与 eda_workflow_guide 同源)',
|
|
43
|
+
'',
|
|
44
|
+
WORKFLOW_SUMMARY,
|
|
45
|
+
'',
|
|
46
|
+
WORKFLOW_GUIDE,
|
|
47
|
+
].join(String.fromCharCode(10))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Line count of the playbook, surfaced for diagnostics and tests. */
|
|
51
|
+
export function skillLineCount(): number {
|
|
52
|
+
return SKILL_PLAYBOOK_LINES.length
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SkillCandidateLike {
|
|
56
|
+
name: string
|
|
57
|
+
description: string
|
|
58
|
+
whenToUse?: string
|
|
59
|
+
invocation: { modelInvocable: boolean; userInvocable: boolean }
|
|
60
|
+
source: string
|
|
61
|
+
provider: string
|
|
62
|
+
rank: number
|
|
63
|
+
locator?: unknown
|
|
64
|
+
metadata?: Record<string, unknown>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function candidate(): SkillCandidateLike {
|
|
68
|
+
return {
|
|
69
|
+
name: EDA_SKILL_NAME,
|
|
70
|
+
description: DESCRIPTION,
|
|
71
|
+
whenToUse: WHEN_TO_USE,
|
|
72
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
73
|
+
source: 'custom',
|
|
74
|
+
provider: EDA_SKILL_PROVIDER,
|
|
75
|
+
rank: EDA_SKILL_RANK,
|
|
76
|
+
locator: { kind: 'inline', skill: EDA_SKILL_NAME },
|
|
77
|
+
metadata: { plugin: 'dsh-eda-mcp', area: 'schematic' },
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The SkillProvider registered on ctx.skills (list + get, read-through). */
|
|
82
|
+
export function createEdaSkillProvider() {
|
|
83
|
+
return {
|
|
84
|
+
name: EDA_SKILL_PROVIDER,
|
|
85
|
+
list: async (): Promise<SkillCandidateLike[]> => [candidate()],
|
|
86
|
+
get: async (input: SkillCandidateLike) => {
|
|
87
|
+
if (input?.name !== EDA_SKILL_NAME) return undefined
|
|
88
|
+
return { ...candidate(), description: DESCRIPTION, content: skillBody() }
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin registers its JLCEDA project playbook as a DSH skill. These assertions
|
|
3
|
+
* pin the provider contract AND the completeness of the manual: every section has to
|
|
4
|
+
* be there, because an incomplete playbook is what makes an agent improvise.
|
|
5
|
+
*
|
|
6
|
+
* `node --experimental-strip-types test/skills.test.ts`
|
|
7
|
+
*/
|
|
8
|
+
import { EDA_SKILL_NAME, EDA_SKILL_PROVIDER, createEdaSkillProvider, skillBody, skillLineCount } from '../src/skills.ts'
|
|
9
|
+
import { SKILL_SECTIONS } from '../src/skills-content.ts'
|
|
10
|
+
|
|
11
|
+
let failures = 0
|
|
12
|
+
function report(label: string, ok: boolean, detail = ''): void {
|
|
13
|
+
if (!ok) failures += 1
|
|
14
|
+
console.log((ok ? 'PASS ' : 'FAIL ') + label + (detail === '' ? '' : ' — ' + detail))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const provider = createEdaSkillProvider()
|
|
18
|
+
const candidates = await provider.list()
|
|
19
|
+
const candidate = candidates[0]
|
|
20
|
+
const body = skillBody()
|
|
21
|
+
|
|
22
|
+
report('provider name is stable', provider.name === EDA_SKILL_PROVIDER, provider.name)
|
|
23
|
+
report('exactly one skill is offered', candidates.length === 1, String(candidates.length))
|
|
24
|
+
report('skill name is kebab-case', /^[a-z0-9]+(-[a-z0-9]+)*$/.test(EDA_SKILL_NAME), EDA_SKILL_NAME)
|
|
25
|
+
report('skill is model and user invocable', candidate.invocation.modelInvocable === true && candidate.invocation.userInvocable === true)
|
|
26
|
+
report('description states a trigger', candidate.description.includes('TRIGGER when'))
|
|
27
|
+
report('description states a non-trigger', candidate.description.includes('DO NOT TRIGGER'))
|
|
28
|
+
report('rank beats the user root (400)', candidate.rank > 400, String(candidate.rank))
|
|
29
|
+
report('playbook has all twelve sections', SKILL_SECTIONS.length === 12, String(SKILL_SECTIONS.length))
|
|
30
|
+
report('playbook is a real manual, not a summary', body.length > 6000 && skillLineCount() > 120, body.length + ' chars / ' + skillLineCount() + ' lines')
|
|
31
|
+
|
|
32
|
+
const definition = await provider.get(candidate)
|
|
33
|
+
report('definition carries the body', definition?.content === body)
|
|
34
|
+
|
|
35
|
+
for (const section of SKILL_SECTIONS) {
|
|
36
|
+
report('section present: ' + section.replace('## ', ''), body.includes(section))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const required = [
|
|
40
|
+
'mcp__jlceda__',
|
|
41
|
+
'eda_import_project',
|
|
42
|
+
'eda_place_net_labels',
|
|
43
|
+
'eda_move_component',
|
|
44
|
+
'eda_export_netlist',
|
|
45
|
+
'eda_run_drc',
|
|
46
|
+
'pcb_import_changes',
|
|
47
|
+
'pcb_draw_board_outline',
|
|
48
|
+
'pcb_create_pour',
|
|
49
|
+
'eda_design_audit',
|
|
50
|
+
'eda_pcb_export_gerber',
|
|
51
|
+
'eda_get_net_labels',
|
|
52
|
+
'eda_list_api_methods',
|
|
53
|
+
'| 现象 | 原因 | 动作 |',
|
|
54
|
+
]
|
|
55
|
+
for (const token of required) {
|
|
56
|
+
report('manual mentions ' + token, body.includes(token))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
report('unknown skill resolves to undefined', (await provider.get({ ...candidate, name: 'nope' })) === undefined)
|
|
60
|
+
|
|
61
|
+
if (failures === 0) console.log('ALL PASS')
|
|
62
|
+
else { console.log(failures + ' FAILED'); process.exitCode = 1 }
|
|
Binary file
|