dsh-plugin-pscad-expert-vyma 1.0.10 → 1.0.12

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.10 | **注册名**:`pscad-automation-expert`
3
+ > **版本**:1.0.12 | **注册名**:`pscad-automation-expert`
4
4
  > **来源**:PSCAD 知识资产固化项目(四阶段:技能文档 → 模板索引 → 工具库 → 插件打包)
5
5
  > 所有知识、工具、模板均来自真实仿真验证(L13~L19 认证轮 + R4 科研轮),**无伪造数据**。
6
6
 
package/index.js CHANGED
@@ -29,7 +29,8 @@ export const Config = Schema.object({
29
29
  .description('Python 解释器路径;默认自动探测(PATH→py 启动器→常见安装位置),无需手动配置'),
30
30
  toolsCli: Schema.string().default('./tools/cli.py'),
31
31
  defaultFolder: Schema.string().default(''),
32
- timeoutMs: Schema.number().default(120000),
32
+ timeoutMs: Schema.number().default(300000)
33
+ .description('单次工具调用总超时(毫秒);PSCAD 冷启动等待上限 180s,需留余量'),
33
34
  })
34
35
 
35
36
  // ---------------- Python 解释器自动探测(免 profile YAML 配置) ----------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pscad-expert-vyma",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
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.10
6
+ version: 1.0.12
7
7
  description: PSCAD 自动化建模与仿真控制专家插件
8
8
 
9
9
  # ---- 系统提示词(加载到 Agent 上下文最前置位置)----
package/tools/main.py CHANGED
@@ -68,7 +68,7 @@ from typing import Any, Dict, List, Optional, Tuple
68
68
  CONFIG_FILE_NAME: str = '.pscad_config.json' # 工作区本地配置文件
69
69
  PSCAD_PORT: int = int(os.environ.get("PSCAD_PORT", "54329"))
70
70
  DEFAULT_FOLDER: str = os.environ.get("PSCAD_WORK_DIR", "") # 无硬编码;空则回退当前工作目录
71
- LAUNCH_WAIT_S: int = 60
71
+ LAUNCH_WAIT_S: int = 180 # PSCAD 冷启动(拉起→自动化端口就绪)实测可能 >60s,放宽到 180s
72
72
  CONNECT_TIMEOUT_S: int = 20
73
73
 
74
74
  # 配置缺失时的标准 Agent 返回(裸字典,供大模型识别并触发对话式引导)
@@ -228,15 +228,31 @@ def init_pscad_environment(exe_path: str, pylib_path: str = '') -> Dict[str, Any
228
228
  """
229
229
  try:
230
230
  if not pylib_path:
231
- discovered = _discover_pylib(exe_path)
232
- if discovered:
233
- pylib_path = discovered
231
+ # ① site-packages 已有 pip 版 mhi-pscad(Python 3.12+ 机器常见):
232
+ # 直接使用(空 pylib 表示不记录路径,运行时优先直导),无需发现
233
+ ok_sp, _ = _trial_import_mhi('')
234
+ if ok_sp:
235
+ pylib_path = ''
234
236
  else:
237
+ discovered = _discover_pylib(exe_path)
238
+ if discovered:
239
+ pylib_path = discovered
240
+ else:
241
+ return {'error': 'ConfigMissing',
242
+ 'instruction': ('未能在 PSCAD 安装目录自动发现 mhi.pscad,'
243
+ 'site-packages 也无可用版本。请确认 PSCAD 5.x '
244
+ '安装完整(应有 bin/win64/mhi.zip),或 '
245
+ 'pip install mhi-pscad 后重试,或手动提供 '
246
+ 'mhi.pscad 库路径后再调用 init_pscad_environment。')}
247
+ # 保存前试导入验证:防止自动发现/指定路径在当前 Python 下不可导入
248
+ # (如 mhi.zip 2.2.1 依赖 Python 3.12 已移除的 distutils)时,把原本
249
+ # 可用的配置覆盖成坏配置。导入失败一律不保存,返回引导。
250
+ if pylib_path:
251
+ ok_imp, imp_msg = _trial_import_mhi(pylib_path)
252
+ if not ok_imp:
235
253
  return {'error': 'ConfigMissing',
236
- 'instruction': ('未能在 PSCAD 安装目录自动发现 mhi.pscad。'
237
- '请确认 PSCAD 5.x 安装完整(应有 '
238
- 'bin/win64/mhi.zip),或手动提供 mhi.pscad '
239
- '库路径后再调用 init_pscad_environment。')}
254
+ 'instruction': ('mhi.pscad 无法在当前 Python 下导入,配置未保存:%s'
255
+ % imp_msg)}
240
256
  r = save_config(exe_path, pylib_path)
241
257
  if r['status'] == 'success':
242
258
  r['data']['message'] = ('PSCAD 环境配置已保存。后续连接将自动使用该配置,'
@@ -246,6 +262,20 @@ def init_pscad_environment(exe_path: str, pylib_path: str = '') -> Dict[str, Any
246
262
  return _err(exc)
247
263
 
248
264
 
265
+ def _trial_import_mhi(pylib: str) -> Tuple[bool, str]:
266
+ """试导入验证:确认 pylib(目录或 zip;空串=仅 site-packages 直导)可导入 mhi.pscad。
267
+
268
+ 不抛异常;失败返回带指引的消息(Python ≥3.12 的 distutils 问题等)。
269
+ """
270
+ try:
271
+ if pylib and pylib not in sys.path:
272
+ sys.path.insert(0, pylib)
273
+ import mhi.pscad # noqa: F401
274
+ return True, ''
275
+ except Exception as exc:
276
+ return False, _mhi_import_error(exc, pylib or '<site-packages>')
277
+
278
+
249
279
  def _cfg_exe() -> Optional[str]:
250
280
  """解析 PSCAD 可执行文件路径:本地配置优先,环境变量降级回退。"""
251
281
  cfg = load_config()
@@ -492,6 +522,57 @@ def _gen_name(comp_type: str) -> str:
492
522
  # ---------------------------------------------------------------------------
493
523
  # 工具集 1:连接与工程加载(懒加载 mhi)
494
524
  # ---------------------------------------------------------------------------
525
+ def _persist_case(prj: Any, project_name: str, folder: str) -> Optional[str]:
526
+ """尽力把会话内新建工程落盘,返回实际 .pscx 路径;找不到返回 None。
527
+
528
+ mhi-pscad 2.9.7 的 create_case(project_name, folder=...) 与 save() 都可能
529
+ 不按 folder 写 .pscx(工程仅在 PSCAD 会话内存)。依次尝试:
530
+ ① save/save_as/save_case 变体;② prj 路径属性;③ 候选目录全盘搜同名文件。
531
+ """
532
+ expected = os.path.join(folder, project_name + '.pscx')
533
+ try:
534
+ # ① 常见保存 API
535
+ for meth, args in (('save', ()), ('save_as', (expected,)),
536
+ ('save_case', (expected,))):
537
+ try:
538
+ fn = getattr(prj, meth)
539
+ fn(*args)
540
+ except Exception:
541
+ continue
542
+ if os.path.isfile(expected):
543
+ return expected
544
+ # ② prj 自带路径属性
545
+ for attr in ('path', 'filename', 'file_name', 'case_path',
546
+ 'project_path'):
547
+ try:
548
+ p = getattr(prj, attr)
549
+ if callable(p):
550
+ p = p()
551
+ if isinstance(p, str) and p.lower().endswith('.pscx') \
552
+ and os.path.isfile(p):
553
+ return p
554
+ except Exception:
555
+ continue
556
+ # ③ 候选目录搜最近生成的同名文件
557
+ import glob as _glob
558
+ bases = [folder, os.getcwd(), os.path.expanduser('~/Documents'),
559
+ os.path.expanduser('~/PSCAD')]
560
+ best = None
561
+ for base in bases:
562
+ try:
563
+ hits = _glob.glob(os.path.join(base, '**',
564
+ project_name + '.pscx'),
565
+ recursive=True)
566
+ except Exception:
567
+ hits = []
568
+ for h in hits:
569
+ if best is None or os.path.getmtime(h) > os.path.getmtime(best):
570
+ best = h
571
+ return best
572
+ except Exception:
573
+ return None
574
+
575
+
495
576
  def connect_and_load_project(project_name: str,
496
577
  folder: Optional[str] = None,
497
578
  create_if_missing: bool = False,
@@ -528,6 +609,7 @@ def connect_and_load_project(project_name: str,
528
609
 
529
610
  folder = folder or (DEFAULT_FOLDER or os.getcwd())
530
611
  _prj_folder = folder
612
+ pscx_path: Optional[str] = None # 实际落盘的 .pscx 路径(探测后回报)
531
613
 
532
614
  prj = None
533
615
  try:
@@ -541,13 +623,23 @@ def connect_and_load_project(project_name: str,
541
623
  try:
542
624
  _pscad.load(pscx.replace('\\', '/'))
543
625
  prj = _pscad.project(project_name)
626
+ pscx_path = pscx
544
627
  except Exception as exc:
545
628
  return _err('加载 %s 失败: %s' % (pscx, exc))
546
629
 
547
630
  if prj is None and create_if_missing:
548
631
  try:
632
+ # 目录不存在可能导致 create_case 静默不落盘:先确保目标目录存在
633
+ try:
634
+ os.makedirs(folder, exist_ok=True)
635
+ except Exception:
636
+ pass
549
637
  _pscad.create_case(project_name, folder=folder)
550
638
  prj = _pscad.project(project_name)
639
+ pscx_path = _persist_case(prj, project_name, folder)
640
+ if pscx_path:
641
+ folder = os.path.dirname(pscx_path) # 会话状态跟随真实落盘目录
642
+ _prj_folder = folder
551
643
  except Exception as exc:
552
644
  return _err('create_case 失败: %s' % exc)
553
645
 
@@ -563,7 +655,13 @@ def connect_and_load_project(project_name: str,
563
655
  except Exception as exc:
564
656
  _main = None
565
657
  return _err('打开 Main 画布失败: %s' % exc)
566
- return _ok({'project': project_name, 'canvas': 'Main'})
658
+ result = {'project': project_name, 'canvas': 'Main',
659
+ 'pscx_path': pscx_path}
660
+ if create_if_missing and not pscx_path:
661
+ result['warning'] = ('create_case 未能在磁盘找到 .pscx(mhi 2.9.7 可能仅'
662
+ '内存建工程):工程仅存在于当前 PSCAD 会话,退出即丢;'
663
+ '建议改用已有模板 .pscx 加载,或提供可写目录。')
664
+ return _ok(result)
567
665
  except Exception as exc:
568
666
  return _err(exc)
569
667