dsh-plugin-pscad-expert-vyma 1.0.2 → 1.0.4
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/cordis.patch.yml +3 -2
- package/index.js +190 -24
- package/package.json +7 -1
- package/plugin.yaml +1 -1
- package/tools/cli.py +103 -0
- package/tools/__pycache__/main.cpython-37.pyc +0 -0
package/cordis.patch.yml
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
- insert:
|
|
2
|
+
- id: pscad-expert-bridge
|
|
3
|
+
name: dsh-plugin-pscad-expert-vyma
|
package/index.js
CHANGED
|
@@ -1,24 +1,190 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
// ============================================================
|
|
2
|
+
// pscad-expert-bridge — DSH 官方插件(Cordis bundle 形态)
|
|
3
|
+
//
|
|
4
|
+
// 将 PSCAD 自动化技能(tools/main.py Python 工具库)以标准
|
|
5
|
+
// defineTool 工具集暴露给 DSH Agent:
|
|
6
|
+
// Node 激活层(本文件)→ 子进程调用 tools/cli.py → Python 工具库
|
|
7
|
+
//
|
|
8
|
+
// 规范要点(dsh-plugin-dev-skill):
|
|
9
|
+
// - ESM 模块 + `name` / `inject` / `Config` / `apply(ctx, config)`
|
|
10
|
+
// - 全部注册经 ctx.tools.register(卸载自动清理,不手动 removeListener)
|
|
11
|
+
// - 所有可调参数进 Config(Schemastery 校验,加载时响亮失败)
|
|
12
|
+
// - execute 返回规范 JSON 值;output.render 为纯函数
|
|
13
|
+
// - 响应 exec.signal 取消(终止 Python 子进程)
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
17
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
18
|
+
import { spawn } from 'node:child_process'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
export const name = 'pscad-expert-bridge'
|
|
22
|
+
export const inject = ['tools']
|
|
23
|
+
|
|
24
|
+
// ---------------- 插件配置(Schemastery,无硬编码) ----------------
|
|
25
|
+
export const Config = Schema.object({
|
|
26
|
+
pythonExecutable: Schema.string().default('python'),
|
|
27
|
+
toolsCli: Schema.string().default('./tools/cli.py'),
|
|
28
|
+
defaultFolder: Schema.string().default(''),
|
|
29
|
+
timeoutMs: Schema.number().default(120000),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// ---------------- Python 桥接(子进程 JSON-RPC,支持取消) ----------------
|
|
33
|
+
function runPy(tool, args, config, signal) {
|
|
34
|
+
const cliPath = fileURLToPath(new URL(config.toolsCli, import.meta.url))
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let settled = false
|
|
37
|
+
const done = (fn, v) => { if (!settled) { settled = true; fn(v) } }
|
|
38
|
+
const proc = spawn(config.pythonExecutable, [cliPath, tool, JSON.stringify(args)], {
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
|
+
})
|
|
42
|
+
let stdout = ''
|
|
43
|
+
let stderr = ''
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
proc.kill('SIGKILL')
|
|
46
|
+
done(reject, new Error(`PSCAD 工具 ${tool} 执行超时(>${config.timeoutMs}ms)`))
|
|
47
|
+
}, config.timeoutMs)
|
|
48
|
+
|
|
49
|
+
const onAbort = () => {
|
|
50
|
+
proc.kill('SIGKILL')
|
|
51
|
+
done(reject, new Error(`工具 ${tool} 已被取消(exec.signal)`))
|
|
52
|
+
}
|
|
53
|
+
if (signal) {
|
|
54
|
+
if (signal.aborted) { onAbort(); return }
|
|
55
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
proc.stdout.on('data', (d) => { stdout += d })
|
|
59
|
+
proc.stderr.on('data', (d) => { stderr += d })
|
|
60
|
+
proc.on('close', (code) => {
|
|
61
|
+
clearTimeout(timer)
|
|
62
|
+
if (signal) signal.removeEventListener('abort', onAbort)
|
|
63
|
+
if (code !== 0) {
|
|
64
|
+
done(reject, new Error(`Python 桥接退出码 ${code}: ${stderr.slice(0, 500)}`))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
done(resolve, JSON.parse(stdout.trim()))
|
|
69
|
+
} catch (err) {
|
|
70
|
+
done(reject, new Error(`Python 桥接返回非 JSON: ${err.message} (stdout: ${stdout.slice(0, 300)})`))
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
proc.on('error', (err) => {
|
|
74
|
+
clearTimeout(timer)
|
|
75
|
+
if (signal) signal.removeEventListener('abort', onAbort)
|
|
76
|
+
done(reject, new Error(`无法启动 Python 子进程: ${err.message}`))
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------- 工具渲染(纯函数:不做 I/O) ----------------
|
|
82
|
+
function renderJson(value) {
|
|
83
|
+
if (value && typeof value === 'object' && value.error === 'ConfigMissing') {
|
|
84
|
+
// 配置缺失:引导 Agent 立即停止当前任务并询问用户路径
|
|
85
|
+
return [{ type: 'text', text: `【配置缺失】${value.instruction}` }]
|
|
86
|
+
}
|
|
87
|
+
if (value && value.status === 'error') {
|
|
88
|
+
return [{ type: 'text', text: `[error] ${value.message}` }]
|
|
89
|
+
}
|
|
90
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------- 工具定义 ----------------
|
|
94
|
+
const toolDefs = [
|
|
95
|
+
{
|
|
96
|
+
name: 'pscad_init_environment',
|
|
97
|
+
description: '首次配置 PSCAD 运行环境:将 PSCAD 可执行文件路径与 mhi.pscad 库目录保存到工作区 .pscad_config.json。任何工具返回 ConfigMissing 时,必须先调用本工具获取用户提供的路径。',
|
|
98
|
+
parameters: {
|
|
99
|
+
exe_path: { type: 'string', required: true, description: 'PSCAD 可执行文件绝对路径(如 C:/PSCAD/bin/win64/Pscad.exe)' },
|
|
100
|
+
pylib_path: { type: 'string', required: true, description: 'mhi.pscad 库目录绝对路径' },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'pscad_connect_load_project',
|
|
105
|
+
description: '启动/连接 PSCAD 实例并加载 .pscx 模板工程(或新建)。连接前自动检查配置,缺失时返回 ConfigMissing 引导。',
|
|
106
|
+
parameters: {
|
|
107
|
+
project_name: { type: 'string', required: true, description: '工程名(不带 .pscx 后缀)' },
|
|
108
|
+
folder: { type: 'string', description: '工程所在目录;缺省用配置默认' },
|
|
109
|
+
create_if_missing: { type: 'boolean', description: '不存在时是否新建(默认 false)' },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'pscad_add_component',
|
|
114
|
+
description: '在当前画布放置 PSCAD 元件并配置参数,强制赋予唯一 Name 标签(后续寻址主键)。返回端口坐标表。',
|
|
115
|
+
parameters: {
|
|
116
|
+
comp_type: { type: 'string', required: true, description: "元件定义名,如 'master:resistor' 或 'resistor'" },
|
|
117
|
+
x: { type: 'integer', required: true, description: '放置锚点 X(网格坐标)' },
|
|
118
|
+
y: { type: 'integer', required: true, description: '放置锚点 Y(网格坐标)' },
|
|
119
|
+
params: { type: 'object', additionalProperties: true, description: '参数键值对(带单位字符串如 100.0 [ohm])' },
|
|
120
|
+
orient: { type: 'integer', description: '旋转取模 4(0/1/2/3)' },
|
|
121
|
+
name: { type: 'string', description: '显式 Name 标签;缺省自动生成唯一名' },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'pscad_connect_ports',
|
|
126
|
+
description: '安全连线:按 Name 寻址两个组件端口,动态取坐标并自动绕行避让(防穿线短路)。返回折线顶点与警告。',
|
|
127
|
+
parameters: {
|
|
128
|
+
source_name: { type: 'string', required: true, description: '源组件 Name 标签' },
|
|
129
|
+
source_port: { type: 'string', required: true, description: '源端口名(如 A、N1、OUT)' },
|
|
130
|
+
target_name: { type: 'string', required: true, description: '目标组件 Name 标签' },
|
|
131
|
+
target_port: { type: 'string', required: true, description: '目标端口名' },
|
|
132
|
+
bend: { type: 'string', description: '绕行策略:auto/hfirst/vfirst/top/bottom(默认 auto)' },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'pscad_connect_signal',
|
|
137
|
+
description: '用同名 datalabel 桥接数据/控制信号(信号链标准做法,无需导线):const→控制端口、表计通道→控制链等。',
|
|
138
|
+
parameters: {
|
|
139
|
+
source_name: { type: 'string', required: true, description: '信号源组件 Name(如 const)' },
|
|
140
|
+
source_port: { type: 'string', required: true, description: '源端口名(如 OUT)' },
|
|
141
|
+
signal_name: { type: 'string', required: true, description: '信号名(唯一,同名即广播)' },
|
|
142
|
+
target_name: { type: 'string', required: true, description: '目标组件 Name(如 source_1)' },
|
|
143
|
+
target_port: { type: 'string', required: true, description: '目标端口名(如 Mag/freq)' },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'pscad_run_simulation',
|
|
148
|
+
description: '配置仿真参数并编译运行 EMTDC,返回完整 build error/warning 消息与 Status code。',
|
|
149
|
+
parameters: {
|
|
150
|
+
time_duration: { type: 'number', description: '仿真时长(秒,默认 0.5)' },
|
|
151
|
+
time_step: { type: 'number', description: 'EMTDC 步长(微秒,默认 10)' },
|
|
152
|
+
sample_step: { type: 'number', description: '采样步长(微秒,默认 50)' },
|
|
153
|
+
timeout_loops: { type: 'integer', description: '等待轮询上限(默认 60)' },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'pscad_read_output',
|
|
158
|
+
description: '读取最近一次仿真通道数据(.inf PGB 映射 + .out 列)。列 RMS=相 RMS;DFT 须用系统实际频率(F0)。',
|
|
159
|
+
parameters: {
|
|
160
|
+
channel: { type: 'string', description: '通道名(如 VrA);缺省返回全部' },
|
|
161
|
+
t0: { type: 'number', description: '分析窗起点(秒,默认 0)' },
|
|
162
|
+
t1: { type: 'number', description: '分析窗终点(缺省取末尾)' },
|
|
163
|
+
method: { type: 'string', description: 'rms(默认)/mean/dft' },
|
|
164
|
+
freq: { type: 'number', description: 'DFT 频率 Hz(默认 50,须等于系统实际频率)' },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'pscad_self_check',
|
|
169
|
+
description: 'PSCAD 插件环境自检:配置状态、路径存在性、工具清单。诊断用。',
|
|
170
|
+
parameters: {},
|
|
171
|
+
},
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
// ---------------- 插件主体 ----------------
|
|
175
|
+
export function apply(ctx, config) {
|
|
176
|
+
for (const def of toolDefs) {
|
|
177
|
+
ctx.tools.register(defineTool({
|
|
178
|
+
name: def.name,
|
|
179
|
+
description: def.description,
|
|
180
|
+
parameters: def.parameters,
|
|
181
|
+
output: {
|
|
182
|
+
schema: { type: 'object' },
|
|
183
|
+
render: renderJson,
|
|
184
|
+
},
|
|
185
|
+
execute: (args, exec) => runPy(def.name, args, config, exec.signal),
|
|
186
|
+
}))
|
|
187
|
+
}
|
|
188
|
+
ctx.logger('pscad-bridge').info(
|
|
189
|
+
`PSCAD Expert 插件已加载:${toolDefs.length} 个工具已注册(Python 桥接: ${config.pythonExecutable} ${config.toolsCli})`)
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-pscad-expert-vyma",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "PSCAD 自动化建模与仿真控制专家插件(DSH Agent 工具库 + 基准模板 + 领域知识)",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "index.js",
|
|
6
7
|
"dsh": {
|
|
7
8
|
"bundle": {
|
|
8
9
|
"patch": "./cordis.patch.yml"
|
|
9
10
|
}
|
|
10
11
|
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@deepseek-ai/cordis": "*",
|
|
14
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
15
|
+
"@deepseek-ai/schemastery": "*"
|
|
16
|
+
},
|
|
11
17
|
"keywords": [
|
|
12
18
|
"dsh",
|
|
13
19
|
"pscad",
|
package/plugin.yaml
CHANGED
package/tools/cli.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""cli.py -- PSCAD 工具库 JSON 桥接入口(Node.js ↔ Python 层)
|
|
3
|
+
|
|
4
|
+
DSH 插件(index.js)通过子进程调用本脚本执行 tools/main.py 中的任意工具函数:
|
|
5
|
+
|
|
6
|
+
python cli.py <tool_name> '<json_args>'
|
|
7
|
+
|
|
8
|
+
- 参数以 JSON 字符串传入(kwargs 匹配函数签名;失败回退位置参数)。
|
|
9
|
+
- 结果以单行 JSON 打印到 stdout(含 status/data 或 error/ConfigMissing 结构)。
|
|
10
|
+
- 本脚本**永不抛异常**:任何错误均转为 JSON 错误字典,保证 Node 侧解析不崩溃。
|
|
11
|
+
|
|
12
|
+
示例::
|
|
13
|
+
|
|
14
|
+
python cli.py init_pscad_environment '{"exe_path": "...", "pylib_path": "..."}'
|
|
15
|
+
python cli.py connect_and_load_project '{"project_name": "my_case"}'
|
|
16
|
+
python cli.py read_output '{"channel": "VrA", "method": "rms"}'
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import traceback
|
|
25
|
+
from typing import Any, Dict, List
|
|
26
|
+
|
|
27
|
+
# 将本文件所在目录加入 sys.path,以便 import main(tools/main.py)
|
|
28
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
29
|
+
|
|
30
|
+
import main as pat # noqa: E402
|
|
31
|
+
|
|
32
|
+
# 白名单:仅暴露经过审核的工具函数(防止任意 Python 代码执行)
|
|
33
|
+
TOOL_WHITELIST: Dict[str, Any] = {
|
|
34
|
+
'init_pscad_environment': pat.init_pscad_environment,
|
|
35
|
+
'connect_and_load_project': pat.connect_and_load_project,
|
|
36
|
+
'add_and_configure_component': pat.add_and_configure_component,
|
|
37
|
+
'set_component_params': pat.set_component_params,
|
|
38
|
+
'get_component': pat.get_component,
|
|
39
|
+
'list_components': pat.list_components,
|
|
40
|
+
'connect_ports_safely': pat.connect_ports_safely,
|
|
41
|
+
'connect_ports_chain': pat.connect_ports_chain,
|
|
42
|
+
'connect_signal_by_name': pat.connect_signal_by_name,
|
|
43
|
+
'register_pgb': pat.register_pgb,
|
|
44
|
+
'check_component_overlaps': pat.check_component_overlaps,
|
|
45
|
+
'configure_simulation': pat.configure_simulation,
|
|
46
|
+
'run_simulation_and_monitor': pat.run_simulation_and_monitor,
|
|
47
|
+
'read_output': pat.read_output,
|
|
48
|
+
'compute_sequence': pat.compute_sequence,
|
|
49
|
+
'restart_pscad': pat.restart_pscad,
|
|
50
|
+
'close_connection': pat.close_connection,
|
|
51
|
+
'build_droop_chain': pat.build_droop_chain,
|
|
52
|
+
'self_check': pat.self_check,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main(argv: List[str]) -> int:
|
|
57
|
+
"""CLI 入口:python cli.py <tool_name> [json_args]。"""
|
|
58
|
+
if len(argv) < 2:
|
|
59
|
+
print(json.dumps({'status': 'error',
|
|
60
|
+
'message': '用法: python cli.py <tool_name> [json_args]'},
|
|
61
|
+
ensure_ascii=False))
|
|
62
|
+
return 2
|
|
63
|
+
|
|
64
|
+
tool = argv[1]
|
|
65
|
+
fn = TOOL_WHITELIST.get(tool)
|
|
66
|
+
if fn is None:
|
|
67
|
+
print(json.dumps({'status': 'error',
|
|
68
|
+
'message': '未知工具: %s(白名单可用: %s)'
|
|
69
|
+
% (tool, sorted(TOOL_WHITELIST))},
|
|
70
|
+
ensure_ascii=False))
|
|
71
|
+
return 2
|
|
72
|
+
|
|
73
|
+
args: Dict[str, Any] = {}
|
|
74
|
+
if len(argv) >= 3:
|
|
75
|
+
try:
|
|
76
|
+
args = json.loads(argv[2])
|
|
77
|
+
if not isinstance(args, dict):
|
|
78
|
+
raise ValueError('args 必须是 JSON 对象')
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
print(json.dumps({'status': 'error',
|
|
81
|
+
'message': '参数 JSON 解析失败: %s' % exc},
|
|
82
|
+
ensure_ascii=False))
|
|
83
|
+
return 2
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
result = fn(**args)
|
|
87
|
+
except TypeError:
|
|
88
|
+
# kwargs 不匹配时回退位置参数(保持参数顺序)
|
|
89
|
+
try:
|
|
90
|
+
result = fn(*args.values())
|
|
91
|
+
except Exception:
|
|
92
|
+
result = {'status': 'error',
|
|
93
|
+
'message': traceback.format_exc(limit=6)}
|
|
94
|
+
except Exception:
|
|
95
|
+
result = {'status': 'error', 'message': traceback.format_exc(limit=6)}
|
|
96
|
+
|
|
97
|
+
# 统一序列化(Component 对象等不可 JSON 序列化的字段降级为字符串)
|
|
98
|
+
print(json.dumps(result, ensure_ascii=False, default=str))
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == '__main__':
|
|
103
|
+
sys.exit(main(sys.argv))
|
|
Binary file
|