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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # pscad-dsh-plugin — PSCAD 自动化建模与仿真控制专家插件
2
2
 
3
- > **版本**:1.0.7 | **注册名**:`pscad-automation-expert`
3
+ > **版本**:1.0.8 | **注册名**:`pscad-automation-expert`
4
4
  > **来源**:PSCAD 知识资产固化项目(四阶段:技能文档 → 模板索引 → 工具库 → 插件打包)
5
5
  > 所有知识、工具、模板均来自真实仿真验证(L13~L19 认证轮 + R4 科研轮),**无伪造数据**。
6
6
 
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 运行环境:将 PSCAD 可执行文件路径与 mhi.pscad 库目录保存到工作区 .pscad_config.json。任何工具返回 ConfigMissing 时,必须先调用本工具获取用户提供的路径。',
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', required: true, description: 'mhi.pscad 库目录绝对路径' },
102
+ pylib_path: { type: 'string', description: 'mhi.pscad 库目录/zip 绝对路径(可选;缺省自动从 PSCAD 安装目录发现)' },
103
103
  },
104
104
  },
105
105
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pscad-expert-vyma",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "PSCAD 自动化建模与仿真控制专家插件(DSH Agent 工具库 + 基准模板 + 领域知识)",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/plugin.yaml CHANGED
@@ -3,7 +3,7 @@
3
3
  # 由知识资产固化项目自动生成(2026-08-25)· 部署至 Git 仓库
4
4
  # ============================================================
5
5
  name: pscad-automation-expert
6
- version: 1.0.7
6
+ version: 1.0.8
7
7
  description: PSCAD 自动化建模与仿真控制专家插件
8
8
 
9
9
  # ---- 系统提示词(加载到 Agent 上下文最前置位置)----
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 可执行文件路径与 mhi.pscad 库目录路径,写入工作区
211
- ``.pscad_config.json``。配置持久化后,连接/启动类工具(connect_and_load_project、
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
- import mhi.pscad # noqa: F401
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
- if _prj is None:
952
- return _err('未加载工程,请先调用 connect_and_load_project()')
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
- if _prj is None:
983
- return _err('未加载工程,请先调用 connect_and_load_project()')
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),