File size: 23,759 Bytes
b6db694
10b8d56
 
b6db694
10b8d56
b6db694
 
 
 
 
10b8d56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6db694
10b8d56
 
b6db694
 
 
 
 
 
 
10b8d56
b6db694
 
 
 
 
10b8d56
b6db694
10b8d56
 
 
 
 
 
 
 
 
b6db694
 
10b8d56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6db694
 
 
10b8d56
b6db694
10b8d56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6db694
10b8d56
b6db694
10b8d56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6db694
10b8d56
 
 
 
 
b6db694
10b8d56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6db694
 
10b8d56
 
 
 
 
 
 
 
 
 
b6db694
10b8d56
b6db694
10b8d56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6db694
10b8d56
b6db694
10b8d56
 
 
b6db694
10b8d56
 
 
 
 
 
 
 
 
b6db694
10b8d56
b6db694
 
10b8d56
 
 
 
 
 
 
 
b6db694
 
10b8d56
b6db694
10b8d56
 
 
 
 
 
 
 
 
 
 
 
 
 
b6db694
10b8d56
b6db694
d0c18f0
10b8d56
d0c18f0
10b8d56
 
 
d0c18f0
 
10b8d56
 
 
d0c18f0
10b8d56
 
 
 
d0c18f0
10b8d56
0d8512e
 
 
 
 
 
 
10b8d56
 
 
0d8512e
 
 
 
 
 
 
 
 
 
 
 
 
 
d0c18f0
10b8d56
 
 
 
d0c18f0
10b8d56
 
 
 
 
d0c18f0
10b8d56
 
b6db694
10b8d56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6db694
 
 
10b8d56
 
 
b6db694
 
10b8d56
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
"""
插件管理器
插件运行时状态事实源,负责启动扫描、状态持久化、详情聚合和启停约束。
"""

import json
import logging
from typing import Dict, List, Optional, Any
from pathlib import Path

from .models import (
    PluginStatus,
    PluginMetadata,
    PluginOperationResult,
    PluginListItem,
    PluginDetail,
    PluginErrorInfo,
    PluginErrorStage,
    PluginGovernanceInfo,
    PluginCategory,
    PluginToolSummary,
    PluginMcpSummary,
    PluginUIEntry,
    PluginUIType,
    UISchemaDefinition,
    UISubmitContract,
    UIFieldDefinition,
)
from .loader import plugin_loader
from .dependency_manager import dependency_manager
from .state_store import get_state_store_manager
from app.config.settings import settings

logger = logging.getLogger(__name__)

# 全局应用引用(由 main.py 在启动时设置)
_app_instance = None


def set_app_instance(app):
    """设置全局应用实例"""
    global _app_instance
    _app_instance = app


class PluginManager:
    """插件管理器

    插件运行时状态事实源,负责:
    - 启动扫描和结构校验
    - 状态持久化(data/plugin_state.json)
    - 详情聚合(依赖、治理、Tool、MCP、UI)
    - 启停约束(依赖检查、加载状态)
    """

    def __init__(self):
        self.plugin_directory = settings.PLUGINS_DIR
        self.state_store = get_state_store_manager(
            settings.DATA_DIR / "plugin_state.json"
        )
        # 插件运行时信息(包含元数据、状态、错误等)
        self._plugins: Dict[str, Dict[str, Any]] = {}
        # 启动扫描
        self._scan_plugins()

    def _scan_plugins(self):
        """启动扫描插件

        分别处理结构校验、依赖检查、加载摘要和 UI entry 检测。
        单个插件失败不阻断其他插件。
        """
        logger.info("扫描插件目录...")

        if not self.plugin_directory.exists():
            logger.warning(f"插件目录不存在: {self.plugin_directory}")
            return

        for plugin_dir in self.plugin_directory.iterdir():
            if not plugin_dir.is_dir():
                continue

            try:
                self._scan_single_plugin(plugin_dir)
            except Exception as e:
                # 单个插件失败不阻断其他插件
                plugin_name = plugin_dir.name
                logger.error(f"扫描插件 {plugin_name} 失败: {e}")
                self._plugins[plugin_name] = {
                    "metadata": None,
                    "status": PluginStatus.ERROR,
                    "install_path": str(plugin_dir),
                    "errors": [
                        PluginErrorInfo(
                            error_code="SCAN_FAILED",
                            message=f"扫描失败: {str(e)}",
                            stage=PluginErrorStage.STRUCTURE,
                            detail=str(e),
                        )
                    ],
                    "dependency_check": None,
                    "governance": None,
                    "tool_summary": PluginToolSummary(),
                    "mcp_summary": PluginMcpSummary(),
                    "ui_entry": None,
                }

        logger.info(f"扫描完成,共 {len(self._plugins)} 个插件")

    def _scan_single_plugin(self, plugin_dir: Path):
        """扫描单个插件

        Args:
            plugin_dir: 插件目录
        """
        plugin_name = plugin_dir.name
        errors: List[PluginErrorInfo] = []

        # 1. 结构校验 - 读取 plugin.json
        metadata, metadata_dict, struct_error = self._validate_structure(plugin_dir)
        if struct_error:
            errors.append(struct_error)
            self._plugins[plugin_name] = {
                "metadata": None,
                "status": PluginStatus.ERROR,
                "install_path": str(plugin_dir),
                "errors": errors,
                "dependency_check": None,
                "governance": None,
                "tool_summary": PluginToolSummary(),
                "mcp_summary": PluginMcpSummary(),
                "ui_entry": None,
            }
            return

        # 2. 依赖检查
        dep_result = dependency_manager.check_plugin_dependencies(
            plugin_path=plugin_dir,
            requirements=metadata_dict.get("requirements", []),
        )

        # 3. 从状态文件获取用户启停状态
        stored_enabled = self.state_store.is_enabled(plugin_name)
        if stored_enabled is None:
            # 首次扫描,从 plugin.json 初始化
            default_enabled = metadata_dict.get("enabled", False)
            self.state_store.initialize_from_metadata(plugin_name, default_enabled)
            stored_enabled = default_enabled

        # 4. 确定初始状态
        if dep_result.overall_status.value in ("missing", "version_mismatch", "check_failed"):
            initial_status = PluginStatus.DEPENDENCY_ERROR
        elif not metadata:
            initial_status = PluginStatus.INCOMPLETE
        else:
            initial_status = PluginStatus.ENABLED if stored_enabled else PluginStatus.DISABLED

        # 5. 检测 UI 入口
        ui_entry = self._detect_ui_entry(plugin_dir, metadata_dict)

        # 6. 构建治理信息
        governance = self._build_governance(metadata_dict, plugin_dir)

        # 7. 尝试加载插件(如果状态允许)
        load_error = None
        if initial_status == PluginStatus.ENABLED:
            try:
                if not plugin_loader.load_plugin_from_dir(plugin_dir):
                    load_error = PluginErrorInfo(
                        error_code="LOAD_FAILED",
                        message="插件加载失败",
                        stage=PluginErrorStage.LOAD,
                    )
                    errors.append(load_error)
                    initial_status = PluginStatus.ERROR
            except Exception as e:
                load_error = PluginErrorInfo(
                    error_code="LOAD_EXCEPTION",
                    message=f"插件加载异常: {str(e)}",
                    stage=PluginErrorStage.LOAD,
                    detail=str(e),
                )
                errors.append(load_error)
                initial_status = PluginStatus.ERROR

        # 8. 存储插件信息
        self._plugins[plugin_name] = {
            "metadata": metadata,
            "metadata_dict": metadata_dict,
            "status": initial_status,
            "install_path": str(plugin_dir),
            "errors": errors,
            "dependency_check": dep_result,
            "governance": governance,
            "tool_summary": PluginToolSummary(),  # T-006 填充
            "mcp_summary": PluginMcpSummary(),  # T-007 填充
            "ui_entry": ui_entry,
        }

        logger.info(f"扫描插件: {plugin_name} (状态: {initial_status.value})")

    def _validate_structure(
        self, plugin_dir: Path
    ) -> tuple:
        """校验插件结构

        Returns:
            (metadata, metadata_dict, error) 元组
        """
        plugin_json_path = plugin_dir / "plugin.json"

        if not plugin_json_path.exists():
            return None, None, PluginErrorInfo(
                error_code="MISSING_PLUGIN_JSON",
                message="缺少 plugin.json 文件",
                stage=PluginErrorStage.STRUCTURE,
            )

        try:
            with open(plugin_json_path, "r", encoding="utf-8") as f:
                content = f.read().strip()
                if not content:
                    return None, None, PluginErrorInfo(
                        error_code="EMPTY_PLUGIN_JSON",
                        message="plugin.json 文件为空",
                        stage=PluginErrorStage.STRUCTURE,
                    )

                metadata_dict = json.loads(content)

        except json.JSONDecodeError as e:
            return None, None, PluginErrorInfo(
                error_code="INVALID_PLUGIN_JSON",
                message=f"plugin.json 格式错误: {str(e)}",
                stage=PluginErrorStage.STRUCTURE,
                detail=str(e),
            )
        except Exception as e:
            return None, None, PluginErrorInfo(
                error_code="READ_PLUGIN_JSON_FAILED",
                message=f"读取 plugin.json 失败: {str(e)}",
                stage=PluginErrorStage.STRUCTURE,
                detail=str(e),
            )

        # 验证元数据(允许部分字段缺失,标记 incomplete)
        try:
            metadata = PluginMetadata(**metadata_dict)
        except Exception as e:
            # 元数据不完整,但不阻断扫描
            logger.warning(f"插件 {plugin_dir.name} 元数据不完整: {e}")
            metadata = None

        return metadata, metadata_dict, None

    def _detect_ui_entry(
        self, plugin_dir: Path, metadata_dict: dict
    ) -> Optional[PluginUIEntry]:
        """检测插件 UI 入口

        Args:
            plugin_dir: 插件目录
            metadata_dict: plugin.json 内容

        Returns:
            PluginUIEntry 或 None
        """
        # 检查是否有静态 UI 文件
        frontend_dir = plugin_dir / "frontend"
        has_static_ui = frontend_dir.exists() and (frontend_dir / "index.html").exists()

        if has_static_ui:
            return PluginUIEntry(
                type=PluginUIType.STATIC,
                entry_path=f"/plugins/{plugin_dir.name}/ui/",
            )

        # 检查是否有 schema UI 声明
        ui_config = metadata_dict.get("ui", {})
        if isinstance(ui_config, dict) and ui_config.get("type") == "schema":
            # 解析 schema UI 配置
            try:
                schema_def = self._parse_schema_ui_config(ui_config, plugin_dir.name)
                submit_def = self._parse_submit_contract(ui_config, plugin_dir.name)
                return PluginUIEntry(
                    type=PluginUIType.SCHEMA,
                    schema=schema_def,
                    submit=submit_def,
                )
            except Exception as e:
                logger.warning(f"解析 schema UI 配置失败 {plugin_dir.name}: {e}")
                return PluginUIEntry(
                    type=PluginUIType.SCHEMA,
                    unavailable_reason=f"Schema UI 配置解析失败: {str(e)}",
                )

        # 无 UI
        return PluginUIEntry(type=PluginUIType.NONE)

    def _parse_schema_ui_config(self, ui_config: dict, plugin_name: str) -> UISchemaDefinition:
        """解析 schema UI 配置

        Args:
            ui_config: plugin.json 中的 ui 配置
            plugin_name: 插件名称

        Returns:
            UISchemaDefinition
        """
        # 解析字段列表
        fields = []
        for field_config in ui_config.get("fields", []):
            if isinstance(field_config, dict):
                fields.append(UIFieldDefinition(
                    name=field_config.get("name", ""),
                    label=field_config.get("label", field_config.get("name", "")),
                    type=field_config.get("type", "string"),
                    required=field_config.get("required", False),
                    default=field_config.get("default"),
                    placeholder=field_config.get("placeholder"),
                    help_text=field_config.get("help_text"),
                    validation=field_config.get("validation"),
                ))

        return UISchemaDefinition(
            title=ui_config.get("title", plugin_name),
            description=ui_config.get("description"),
            fields=fields,
            result_schema=ui_config.get("result_schema"),
        )

    def _parse_submit_contract(self, ui_config: dict, plugin_name: str) -> UISubmitContract:
        """解析提交契约

        Args:
            ui_config: plugin.json 中的 ui 配置
            plugin_name: 插件名称

        Returns:
            UISubmitContract
        """
        submit_config = ui_config.get("submit", {})
        if isinstance(submit_config, dict):
            return UISubmitContract(
                method=submit_config.get("method", "POST"),
                path=submit_config.get("path", f"/plugins/{plugin_name}/api/run"),
                content_type=submit_config.get("content_type", "application/json"),
                success_path=submit_config.get("success_path"),
                error_path=submit_config.get("error_path"),
            )

        # 默认提交契约
        return UISubmitContract(
            method="POST",
            path=f"/plugins/{plugin_name}/api/run",
            content_type="application/json",
        )

    def _build_governance(
        self, metadata_dict: dict, plugin_dir: Path
    ) -> PluginGovernanceInfo:
        """构建插件治理信息

        Args:
            metadata_dict: plugin.json 内容
            plugin_dir: 插件目录

        Returns:
            PluginGovernanceInfo
        """
        # 解析分类
        category_str = metadata_dict.get("category", "other")
        try:
            category = PluginCategory(category_str)
        except ValueError:
            category = PluginCategory.OTHER

        # 解析能力标签
        capabilities = metadata_dict.get("capabilities", [])

        # 检查是否有 MCP 工具
        has_mcp = (plugin_dir / "mcp.py").exists()

        # 检查是否有 API 路由
        has_api = (plugin_dir / "api.py").exists()

        # 检查是否有 UI
        frontend_dir = plugin_dir / "frontend"
        has_ui = frontend_dir.exists() and (frontend_dir / "index.html").exists()

        return PluginGovernanceInfo(
            category=category,
            capabilities=capabilities,
            has_mcp_tools=has_mcp,
            has_api_routes=has_api,
            has_ui=has_ui,
        )

    def get_plugin_list(self) -> List[PluginListItem]:
        """获取插件列表

        Returns:
            PluginListItem 列表
        """
        result = []
        for plugin_name, info in self._plugins.items():
            try:
                item = PluginListItem(
                    metadata=info.get("metadata") or PluginMetadata(
                        name=plugin_name,
                        version="unknown",
                        description="元数据不完整",
                        author="unknown",
                    ),
                    status=info["status"],
                    dependency_summary=info.get("dependency_check"),
                    governance_summary=info.get("governance"),
                    tool_summary=info.get("tool_summary", PluginToolSummary()),
                    mcp_summary=info.get("mcp_summary", PluginMcpSummary()),
                    ui_entry=info.get("ui_entry"),
                    errors=info.get("errors", []),
                )
                result.append(item)
            except Exception as e:
                logger.error(f"构建插件 {plugin_name} 列表项失败: {e}")
                # 单个插件失败不阻断列表
        return result

    def get_enabled_plugins(self) -> List[PluginListItem]:
        """获取已启用的插件列表

        Returns:
            状态为 ENABLED 的 PluginListItem 列表
        """
        all_plugins = self.get_plugin_list()
        return [p for p in all_plugins if p.status == PluginStatus.ENABLED]

    def get_plugin_detail(self, plugin_name: str) -> Optional[PluginDetail]:
        """获取插件详情

        Args:
            plugin_name: 插件名

        Returns:
            PluginDetail 或 None
        """
        info = self._plugins.get(plugin_name)
        if info is None:
            return None

        try:
            return PluginDetail(
                metadata=info.get("metadata") or PluginMetadata(
                    name=plugin_name,
                    version="unknown",
                    description="元数据不完整",
                    author="unknown",
                ),
                status=info["status"],
                install_path=info["install_path"],
                dependency_check=info.get("dependency_check"),
                governance=info.get("governance"),
                tools=[],  # T-006 填充
                mcp_tools=[],  # T-007 填充
                ui_entry=info.get("ui_entry"),
                errors=info.get("errors", []),
            )
        except Exception as e:
            logger.error(f"构建插件 {plugin_name} 详情失败: {e}")
            return None

    def enable_plugin(self, plugin_name: str) -> PluginOperationResult:
        """启用插件

        启用前检查结构、依赖和加载状态。
        """
        info = self._plugins.get(plugin_name)
        if info is None:
            return PluginOperationResult.error_result(
                plugin_name=plugin_name, error="插件不存在"
            )

        current_status = info["status"]

        # 检查是否可以启用
        if current_status == PluginStatus.ERROR:
            return PluginOperationResult.error_result(
                plugin_name=plugin_name, error="插件有结构或加载错误,无法启用"
            )

        if current_status == PluginStatus.DEPENDENCY_ERROR:
            return PluginOperationResult.error_result(
                plugin_name=plugin_name, error="插件依赖缺失或版本不匹配,无法启用"
            )

        if current_status == PluginStatus.ENABLED:
            return PluginOperationResult.error_result(
                plugin_name=plugin_name, error="插件已启用"
            )

        # 尝试加载插件
        plugin_dir = Path(info["install_path"])
        try:
            if not plugin_loader.load_plugin_from_dir(plugin_dir):
                info["status"] = PluginStatus.ERROR
                info["errors"].append(
                    PluginErrorInfo(
                        error_code="LOAD_FAILED",
                        message="插件加载失败",
                        stage=PluginErrorStage.LOAD,
                    )
                )
                return PluginOperationResult.error_result(
                    plugin_name=plugin_name, error="插件加载失败"
                )
        except Exception as e:
            info["status"] = PluginStatus.ERROR
            info["errors"].append(
                PluginErrorInfo(
                    error_code="LOAD_EXCEPTION",
                    message=f"插件加载异常: {str(e)}",
                    stage=PluginErrorStage.LOAD,
                    detail=str(e),
                )
            )
            return PluginOperationResult.error_result(
                plugin_name=plugin_name, error=f"插件加载异常: {str(e)}"
            )

        # 更新状态
        info["status"] = PluginStatus.ENABLED
        self.state_store.set_enabled(plugin_name, True, updated_by="user")

        logger.info(f"插件 {plugin_name} 已启用")
        return PluginOperationResult.success_result(
            plugin_name=plugin_name, message=f"插件 {plugin_name} 已启用"
        )

    def disable_plugin(self, plugin_name: str) -> PluginOperationResult:
        """禁用插件"""
        info = self._plugins.get(plugin_name)
        if info is None:
            return PluginOperationResult.error_result(
                plugin_name=plugin_name, error="插件不存在"
            )

        current_status = info["status"]

        if current_status == PluginStatus.DISABLED:
            return PluginOperationResult.error_result(
                plugin_name=plugin_name, error="插件已禁用"
            )

        # 更新状态
        info["status"] = PluginStatus.DISABLED
        self.state_store.set_enabled(plugin_name, False, updated_by="user")

        logger.info(f"插件 {plugin_name} 已禁用")
        return PluginOperationResult.success_result(
            plugin_name=plugin_name, message=f"插件 {plugin_name} 已禁用"
        )

    def get_plugin_status(self, plugin_name: str) -> Optional[PluginStatus]:
        """获取插件状态。

        Phase 2.10:可选 readiness hook。
        若插件实例实现了 check_readiness() 且返回 ok=False,
        当基础状态为 ENABLED 时降级为 DEPENDENCY_ERROR。
        hook 抛任何异常都忽略,绝不阻断其他插件。
        """
        info = self._plugins.get(plugin_name)
        if info is None:
            return None
        base_status = info["status"]
        # readiness hook 严格隔离
        if base_status == PluginStatus.ENABLED:
            try:
                from app.plugins.loader import plugin_loader
                instance = plugin_loader.get_plugin_instance(plugin_name)
                if instance is not None and hasattr(instance, "check_readiness"):
                    readiness = instance.check_readiness()
                    if isinstance(readiness, dict) and readiness.get("ok") is False:
                        return PluginStatus.DEPENDENCY_ERROR
            except Exception:
                # hook 异常绝不能影响插件状态查询
                pass
        return base_status

    def is_plugin_available(self, plugin_name: str) -> bool:
        """检查插件是否可用(用于 MCP 门控)"""
        status = self.get_plugin_status(plugin_name)
        return status == PluginStatus.ENABLED

    def get_unavailable_reason(self, plugin_name: str) -> Optional[str]:
        """获取插件不可用原因(用于 MCP 错误反馈)"""
        info = self._plugins.get(plugin_name)
        if info is None:
            return "插件不存在"

        status = info["status"]
        if status == PluginStatus.ENABLED:
            return None

        if status == PluginStatus.DISABLED:
            return "插件已禁用"

        if status == PluginStatus.DEPENDENCY_ERROR:
            dep_check = info.get("dependency_check")
            if dep_check:
                return f"依赖问题: {dep_check.overall_status.value}"
            return "依赖检查失败"

        if status == PluginStatus.ERROR:
            errors = info.get("errors", [])
            if errors:
                return f"错误: {errors[0].message}"
            return "插件错误"

        if status == PluginStatus.INCOMPLETE:
            return "插件元数据不完整"

        return f"插件状态: {status.value}"

    def get_system_plugin_summary(self) -> Dict[str, Any]:
        """获取系统页插件摘要

        Returns:
            包含插件统计和状态的字典
        """
        total = len(self._plugins)
        enabled = sum(
            1 for info in self._plugins.values()
            if info["status"] == PluginStatus.ENABLED
        )
        disabled = sum(
            1 for info in self._plugins.values()
            if info["status"] == PluginStatus.DISABLED
        )
        error = sum(
            1 for info in self._plugins.values()
            if info["status"] in (
                PluginStatus.ERROR,
                PluginStatus.DEPENDENCY_ERROR,
                PluginStatus.INCOMPLETE,
            )
        )

        return {
            "total": total,
            "enabled": enabled,
            "disabled": disabled,
            "error": error,
            "plugin_directory": str(self.plugin_directory),
        }


# 创建全局插件管理器实例(延迟初始化)
plugin_manager: Optional[PluginManager] = None


def get_plugin_manager() -> PluginManager:
    """获取插件管理器实例"""
    global plugin_manager
    if plugin_manager is None:
        plugin_manager = PluginManager()
    return plugin_manager