dsh-plugin-pscad-expert-vyma 1.0.7 → 1.0.8
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/README.md +1 -1
- package/index.js +2 -2
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/tools/main.py +158 -12
package/README.md
CHANGED
package/index.js
CHANGED
|
@@ -96,10 +96,10 @@ function renderJson(_args, value) {
|
|
|
96
96
|
const toolDefs = [
|
|
97
97
|
{
|
|
98
98
|
name: 'pscad_init_environment',
|
|
99
|
-
description: '首次配置 PSCAD
|
|
99
|
+
description: '首次配置 PSCAD 运行环境:保存 PSCAD 可执行文件路径(必填)与 mhi.pscad 库位置(可选)到用户级 .pscad_config.json。pylib 缺省时自动从 PSCAD 安装目录发现(PSCAD 5.x 自带 bin/win64/mhi.zip),只有 PSCAD + Python 的机器无需额外文件。任何工具返回 ConfigMissing 时,必须先调用本工具获取用户提供的路径。',
|
|
100
100
|
parameters: {
|
|
101
101
|
exe_path: { type: 'string', required: true, description: 'PSCAD 可执行文件绝对路径(如 C:/PSCAD/bin/win64/Pscad.exe)' },
|
|
102
|
-
pylib_path: { type: 'string',
|
|
102
|
+
pylib_path: { type: 'string', description: 'mhi.pscad 库目录/zip 绝对路径(可选;缺省自动从 PSCAD 安装目录发现)' },
|
|
103
103
|
},
|
|
104
104
|
},
|
|
105
105
|
{
|
package/package.json
CHANGED
package/plugin.yaml
CHANGED
package/tools/main.py
CHANGED
|
@@ -194,6 +194,9 @@ def save_config(exe_path: str, pylib_path: str) -> Dict[str, Any]:
|
|
|
194
194
|
'saved_at': time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
195
195
|
}
|
|
196
196
|
path = _config_path()
|
|
197
|
+
parent = os.path.dirname(path)
|
|
198
|
+
if parent and not os.path.isdir(parent):
|
|
199
|
+
os.makedirs(parent, exist_ok=True)
|
|
197
200
|
with open(path, 'w', encoding='utf-8') as fh:
|
|
198
201
|
json.dump(cfg, fh, ensure_ascii=False, indent=2)
|
|
199
202
|
_config_cache = cfg
|
|
@@ -204,16 +207,19 @@ def save_config(exe_path: str, pylib_path: str) -> Dict[str, Any]:
|
|
|
204
207
|
return _err(exc)
|
|
205
208
|
|
|
206
209
|
|
|
207
|
-
def init_pscad_environment(exe_path: str, pylib_path: str) -> Dict[str, Any]:
|
|
210
|
+
def init_pscad_environment(exe_path: str, pylib_path: str = '') -> Dict[str, Any]:
|
|
208
211
|
"""**初始化工具**:配置 PSCAD 运行环境并持久化(AI 首个应调用的工具)。
|
|
209
212
|
|
|
210
|
-
接收用户提供的 PSCAD
|
|
211
|
-
``.pscad_config.json
|
|
212
|
-
restart_pscad
|
|
213
|
+
接收用户提供的 PSCAD 可执行文件路径(**必填**)与 mhi.pscad 库位置(可选)。
|
|
214
|
+
写入用户级 ``.pscad_config.json``。配置持久化后,连接/启动类工具
|
|
215
|
+
(connect_and_load_project、restart_pscad 等)将自动懒加载该配置,
|
|
216
|
+
无需任何系统环境变量。
|
|
213
217
|
|
|
214
218
|
Args:
|
|
215
|
-
exe_path: PSCAD
|
|
216
|
-
pylib_path: mhi.pscad
|
|
219
|
+
exe_path: PSCAD 可执行文件绝对路径(必填)。
|
|
220
|
+
pylib_path: mhi.pscad 库目录/zip 绝对路径(可选)。**留空时自动从
|
|
221
|
+
PSCAD 安装目录发现**(PSCAD 5.x 自带 ``bin/win64/mhi.zip``),
|
|
222
|
+
因此只有 PSCAD + Python 的机器无需准备任何额外文件。
|
|
217
223
|
|
|
218
224
|
Returns:
|
|
219
225
|
success: data 含 ``config_path``、``pscad_exe_path``、``mhi_pylib_path``
|
|
@@ -221,6 +227,16 @@ def init_pscad_environment(exe_path: str, pylib_path: str) -> Dict[str, Any]:
|
|
|
221
227
|
error: 写入失败。
|
|
222
228
|
"""
|
|
223
229
|
try:
|
|
230
|
+
if not pylib_path:
|
|
231
|
+
discovered = _discover_pylib(exe_path)
|
|
232
|
+
if discovered:
|
|
233
|
+
pylib_path = discovered
|
|
234
|
+
else:
|
|
235
|
+
return {'error': 'ConfigMissing',
|
|
236
|
+
'instruction': ('未能在 PSCAD 安装目录自动发现 mhi.pscad。'
|
|
237
|
+
'请确认 PSCAD 5.x 安装完整(应有 '
|
|
238
|
+
'bin/win64/mhi.zip),或手动提供 mhi.pscad '
|
|
239
|
+
'库路径后再调用 init_pscad_environment。')}
|
|
224
240
|
r = save_config(exe_path, pylib_path)
|
|
225
241
|
if r['status'] == 'success':
|
|
226
242
|
r['data']['message'] = ('PSCAD 环境配置已保存。后续连接将自动使用该配置,'
|
|
@@ -239,12 +255,54 @@ def _cfg_exe() -> Optional[str]:
|
|
|
239
255
|
return exe or None
|
|
240
256
|
|
|
241
257
|
|
|
258
|
+
def _discover_pylib(exe: Optional[str]) -> Optional[str]:
|
|
259
|
+
"""从 PSCAD 安装目录**自动发现** mhi.pscad,无需用户准备任何文件。
|
|
260
|
+
|
|
261
|
+
PSCAD 5.x 内嵌 Python API:mhi 包以 ``<PSCAD>/bin/win64/mhi.zip`` 形式随
|
|
262
|
+
安装分发(zip 可直接加入 sys.path 被 ``import mhi.pscad`` 解析)。依次尝试:
|
|
263
|
+
exe 同目录 ``mhi.zip`` / ``mhi`` / ``pylib`` → 安装根 ``pylib`` →
|
|
264
|
+
安装根 ``Lib/site-packages``。返回候选路径(目录或 zip 文件),找不到 None。
|
|
265
|
+
"""
|
|
266
|
+
if not exe:
|
|
267
|
+
return None
|
|
268
|
+
try:
|
|
269
|
+
exe_dir = os.path.dirname(os.path.abspath(exe))
|
|
270
|
+
root = os.path.dirname(exe_dir)
|
|
271
|
+
candidates = [
|
|
272
|
+
os.path.join(exe_dir, 'mhi.zip'),
|
|
273
|
+
os.path.join(exe_dir, 'mhi'),
|
|
274
|
+
os.path.join(exe_dir, 'pylib'),
|
|
275
|
+
os.path.join(root, 'pylib'),
|
|
276
|
+
os.path.join(root, 'Lib', 'site-packages'),
|
|
277
|
+
]
|
|
278
|
+
import zipfile
|
|
279
|
+
for cand in candidates:
|
|
280
|
+
try:
|
|
281
|
+
if os.path.isfile(cand) and cand.lower().endswith('.zip'):
|
|
282
|
+
with zipfile.ZipFile(cand) as zf:
|
|
283
|
+
if 'mhi/__init__.py' in set(zf.namelist()):
|
|
284
|
+
return cand
|
|
285
|
+
elif (os.path.isdir(cand) and os.path.isfile(
|
|
286
|
+
os.path.join(cand, 'mhi', '__init__.py'))):
|
|
287
|
+
return cand
|
|
288
|
+
except Exception:
|
|
289
|
+
continue
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
242
295
|
def _cfg_pylib() -> Optional[str]:
|
|
243
|
-
"""解析 mhi.pscad
|
|
296
|
+
"""解析 mhi.pscad 库位置:配置优先 → 环境变量 → **PSCAD 安装自动发现**。
|
|
297
|
+
|
|
298
|
+
自动发现使插件在**只有 PSCAD + Python** 的机器上即可使用,无需预置 pylib。
|
|
299
|
+
"""
|
|
244
300
|
cfg = load_config()
|
|
245
301
|
pylib = cfg.get('mhi_pylib_path')
|
|
246
302
|
if not pylib:
|
|
247
303
|
pylib = os.environ.get('MHI_PYLIB_PATH')
|
|
304
|
+
if not pylib:
|
|
305
|
+
pylib = _discover_pylib(_cfg_exe())
|
|
248
306
|
return pylib or None
|
|
249
307
|
|
|
250
308
|
|
|
@@ -300,6 +358,8 @@ def _import_mhi() -> Tuple[bool, str]:
|
|
|
300
358
|
流程:``load_config()`` 取 pylib 路径 → 动态加入 ``sys.path`` → import。
|
|
301
359
|
**配置缺失时不抛异常**,返回引导消息:先调用 ``init_pscad_environment``。
|
|
302
360
|
"""
|
|
361
|
+
global mhi # 必须绑定到模块级:连接代码(_ensure_connected/_launch_pscad)
|
|
362
|
+
# 依赖 mhi.pscad.connect(...),函数内 import 只会绑定局部名导致 NameError
|
|
303
363
|
pylib = _cfg_pylib()
|
|
304
364
|
if not pylib:
|
|
305
365
|
return False, MISSING_CONFIG_MSG
|
|
@@ -307,7 +367,7 @@ def _import_mhi() -> Tuple[bool, str]:
|
|
|
307
367
|
if 'mhi.pscad' not in sys.modules:
|
|
308
368
|
if pylib not in sys.path:
|
|
309
369
|
sys.path.insert(0, pylib)
|
|
310
|
-
|
|
370
|
+
import mhi.pscad # noqa: F401 幂等:已加载时仅查 sys.modules 并绑定名字
|
|
311
371
|
return True, ''
|
|
312
372
|
except Exception as exc:
|
|
313
373
|
return False, ('import mhi.pscad 失败(mhi_pylib_path=%s): %s'
|
|
@@ -474,6 +534,7 @@ def connect_and_load_project(project_name: str,
|
|
|
474
534
|
|
|
475
535
|
_prj = prj
|
|
476
536
|
_prj_name = project_name
|
|
537
|
+
_save_session() # 落盘会话,供后续子进程自愈重连
|
|
477
538
|
try:
|
|
478
539
|
_main = prj.canvas('Main')
|
|
479
540
|
except Exception as exc:
|
|
@@ -503,6 +564,78 @@ def _ensure_connected() -> Tuple[bool, str]:
|
|
|
503
564
|
return False, '连接 PSCAD 失败(port=%d): %s' % (PSCAD_PORT, exc)
|
|
504
565
|
|
|
505
566
|
|
|
567
|
+
# ---------------------------------------------------------------------------
|
|
568
|
+
# 会话状态持久化 + 自愈重连
|
|
569
|
+
# 每次工具调用都是独立 cli.py 子进程,main.py 内存句柄(_pscad/_prj/_main)
|
|
570
|
+
# 跨调用即丢失;connect_and_load_project 成功后落盘会话,状态工具启动时
|
|
571
|
+
# 用 _ensure_project() 从 PSCAD 实例重取工程句柄,实现多步工作流无缝衔接。
|
|
572
|
+
# ---------------------------------------------------------------------------
|
|
573
|
+
def _session_path() -> str:
|
|
574
|
+
"""会话状态文件:与配置同目录(随 DSH_HOME 环境一致)。"""
|
|
575
|
+
return os.path.join(os.path.dirname(_config_path()), 'pscad_session.json')
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _save_session() -> None:
|
|
579
|
+
"""落盘当前工程会话(project_name + folder)。失败静默(非关键路径)。"""
|
|
580
|
+
try:
|
|
581
|
+
with open(_session_path(), 'w', encoding='utf-8') as fh:
|
|
582
|
+
json.dump({'project_name': _prj_name, 'folder': _prj_folder}, fh)
|
|
583
|
+
except Exception:
|
|
584
|
+
pass
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _load_session() -> Tuple[Optional[str], Optional[str]]:
|
|
588
|
+
"""读取会话状态;无/损坏返回 (None, None)。"""
|
|
589
|
+
try:
|
|
590
|
+
with open(_session_path(), encoding='utf-8') as fh:
|
|
591
|
+
d = json.load(fh)
|
|
592
|
+
return d.get('project_name'), d.get('folder')
|
|
593
|
+
except Exception:
|
|
594
|
+
return None, None
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _ensure_project() -> Optional[Dict[str, Any]]:
|
|
598
|
+
"""自愈:若本进程未持有工程句柄,按会话状态重连 PSCAD 并重新获取。
|
|
599
|
+
|
|
600
|
+
成功返回 None;失败返回 error 字典(含引导消息)。所有状态型工具
|
|
601
|
+
(configure/run/read/canvas 类)入口都应先调用本函数。
|
|
602
|
+
"""
|
|
603
|
+
global _pscad, _prj, _prj_name, _prj_folder, _main
|
|
604
|
+
if _prj is not None and _main is not None:
|
|
605
|
+
return None
|
|
606
|
+
name, folder = _load_session()
|
|
607
|
+
if not name or not folder:
|
|
608
|
+
return _err('未加载工程且无会话状态,请先调用 connect_and_load_project()')
|
|
609
|
+
missing = _require_config()
|
|
610
|
+
if missing is not None:
|
|
611
|
+
return missing
|
|
612
|
+
ok, msg = _ensure_connected()
|
|
613
|
+
if not ok:
|
|
614
|
+
return _err(msg)
|
|
615
|
+
_prj_folder = folder
|
|
616
|
+
try:
|
|
617
|
+
prj = _pscad.project(name)
|
|
618
|
+
except Exception:
|
|
619
|
+
prj = None
|
|
620
|
+
if prj is None:
|
|
621
|
+
pscx = os.path.join(folder, name + '.pscx')
|
|
622
|
+
if os.path.isfile(pscx):
|
|
623
|
+
try:
|
|
624
|
+
_pscad.load(pscx.replace('\\', '/'))
|
|
625
|
+
prj = _pscad.project(name)
|
|
626
|
+
except Exception as exc:
|
|
627
|
+
return _err('会话工程重载失败(%s): %s' % (pscx, exc))
|
|
628
|
+
if prj is None:
|
|
629
|
+
return _err('会话工程 "%s" 不可用(folder=%s)' % (name, folder))
|
|
630
|
+
_prj = prj
|
|
631
|
+
_prj_name = name
|
|
632
|
+
try:
|
|
633
|
+
_main = prj.canvas('Main')
|
|
634
|
+
except Exception:
|
|
635
|
+
_main = None
|
|
636
|
+
return None
|
|
637
|
+
|
|
638
|
+
|
|
506
639
|
def _launch_pscad() -> Tuple[bool, str]:
|
|
507
640
|
"""拉起 PSCAD 进程并等待自动化端口就绪(崩溃恢复标准路径;懒加载 exe 路径)。"""
|
|
508
641
|
global _pscad
|
|
@@ -577,6 +710,10 @@ def close_connection() -> Dict[str, Any]:
|
|
|
577
710
|
# ---------------------------------------------------------------------------
|
|
578
711
|
def _require_canvas() -> Optional[Dict[str, Any]]:
|
|
579
712
|
"""前置检查:已加载工程且 Main 画布可用。失败返回 error dict,否则 None。"""
|
|
713
|
+
if _main is None:
|
|
714
|
+
err = _ensure_project() # 跨进程自愈:按会话状态重连取回画布
|
|
715
|
+
if err is not None:
|
|
716
|
+
return err
|
|
580
717
|
if _main is None:
|
|
581
718
|
return _err('尚未加载工程画布,请先调用 connect_and_load_project()')
|
|
582
719
|
return None
|
|
@@ -948,8 +1085,9 @@ def configure_simulation(time_duration: float = 0.5,
|
|
|
948
1085
|
error: 未加载工程。
|
|
949
1086
|
"""
|
|
950
1087
|
try:
|
|
951
|
-
|
|
952
|
-
|
|
1088
|
+
err = _ensure_project()
|
|
1089
|
+
if err is not None:
|
|
1090
|
+
return err
|
|
953
1091
|
out = output_filename or ('%s.out' % _prj_name)
|
|
954
1092
|
_prj.parameters(time_duration=time_duration, time_step=time_step,
|
|
955
1093
|
sample_step=sample_step, PlotType=1,
|
|
@@ -979,8 +1117,9 @@ def run_simulation_and_monitor(timeout_loops: int = 60,
|
|
|
979
1117
|
error: 未加载工程/调用异常。
|
|
980
1118
|
"""
|
|
981
1119
|
try:
|
|
982
|
-
|
|
983
|
-
|
|
1120
|
+
err = _ensure_project()
|
|
1121
|
+
if err is not None:
|
|
1122
|
+
return err
|
|
984
1123
|
_prj.save()
|
|
985
1124
|
_prj.build()
|
|
986
1125
|
|
|
@@ -1056,6 +1195,9 @@ def read_output(channel: Optional[str] = None,
|
|
|
1056
1195
|
error: 无输出/通道不存在。
|
|
1057
1196
|
"""
|
|
1058
1197
|
try:
|
|
1198
|
+
err = _ensure_project()
|
|
1199
|
+
if err is not None:
|
|
1200
|
+
return err
|
|
1059
1201
|
gf = _gf_dir()
|
|
1060
1202
|
if gf is None:
|
|
1061
1203
|
return _err('未找到 .gf46 输出目录(工程: %s,folder: %s)——'
|
|
@@ -1231,6 +1373,7 @@ def self_check() -> Dict[str, Any]:
|
|
|
1231
1373
|
try:
|
|
1232
1374
|
cfg = load_config()
|
|
1233
1375
|
exe = _cfg_exe()
|
|
1376
|
+
cfg_pylib = cfg.get('mhi_pylib_path') or os.environ.get('MHI_PYLIB_PATH')
|
|
1234
1377
|
pylib = _cfg_pylib()
|
|
1235
1378
|
ok_import, msg = _import_mhi()
|
|
1236
1379
|
return _ok({
|
|
@@ -1239,6 +1382,9 @@ def self_check() -> Dict[str, Any]:
|
|
|
1239
1382
|
'config_path': _config_path(),
|
|
1240
1383
|
'resolved': {'pscad_exe_path': exe, 'mhi_pylib_path': pylib,
|
|
1241
1384
|
'pscad_port': PSCAD_PORT},
|
|
1385
|
+
'pylib_source': ('config' if cfg_pylib else
|
|
1386
|
+
('env' if os.environ.get('MHI_PYLIB_PATH') else
|
|
1387
|
+
('auto-discovered' if pylib else 'missing'))),
|
|
1242
1388
|
'mhi_import': ok_import,
|
|
1243
1389
|
'mhi_error': '' if ok_import else msg,
|
|
1244
1390
|
'pscad_exe_exists': bool(exe) and os.path.isfile(exe),
|