devmate 1.0.0__py3-none-any.whl
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.
- devmate/__init__.py +6 -0
- devmate/__main__.py +5 -0
- devmate/app/__init__.py +1 -0
- devmate/app/cli.py +1663 -0
- devmate/app/tui.py +291 -0
- devmate/app/web/__init__.py +95 -0
- devmate/bus/__init__.py +1 -0
- devmate/bus/bus.py +151 -0
- devmate/bus/command.py +84 -0
- devmate/core/__init__.py +1 -0
- devmate/core/config.py +115 -0
- devmate/core/logging.py +54 -0
- devmate/core/security.py +61 -0
- devmate/events/__init__.py +1 -0
- devmate/events/bus.py +97 -0
- devmate/events/domain_event.py +37 -0
- devmate/module/__init__.py +1 -0
- devmate/module/registry.py +74 -0
- devmate/security/audit.py +96 -0
- devmate/security/vault.py +221 -0
- devmate-1.0.0.dist-info/METADATA +116 -0
- devmate-1.0.0.dist-info/RECORD +37 -0
- devmate-1.0.0.dist-info/WHEEL +5 -0
- devmate-1.0.0.dist-info/entry_points.txt +18 -0
- devmate-1.0.0.dist-info/top_level.txt +13 -0
- devmate_agent/__init__.py +440 -0
- devmate_apidev/__init__.py +271 -0
- devmate_apihub/__init__.py +313 -0
- devmate_dbadmin/__init__.py +263 -0
- devmate_gitflow/__init__.py +247 -0
- devmate_monitor/__init__.py +112 -0
- devmate_notekeeper/__init__.py +280 -0
- devmate_regexlab/__init__.py +212 -0
- devmate_reporter/__init__.py +170 -0
- devmate_scaffold/__init__.py +181 -0
- devmate_sshman/__init__.py +330 -0
- devmate_toolkit/__init__.py +223 -0
devmate/core/config.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""配置管理——Pydantic + YAML + 环境变量"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic_settings import BaseSettings
|
|
10
|
+
|
|
11
|
+
# ── 路径常量 ─────────────────────────────────────────
|
|
12
|
+
DEVMATE_HOME = Path.home() / ".devmate"
|
|
13
|
+
CONFIG_PATH = DEVMATE_HOME / "config.yml"
|
|
14
|
+
DATA_PATH = DEVMATE_HOME / "data.db"
|
|
15
|
+
VAULT_PATH = DEVMATE_HOME / "vault"
|
|
16
|
+
LOG_PATH = DEVMATE_HOME / "devmate.log"
|
|
17
|
+
AUDIT_PATH = DEVMATE_HOME / "audit.log"
|
|
18
|
+
EVENT_STORE_PATH = DEVMATE_HOME / "event_store"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── 安全配置 ─────────────────────────────────────────
|
|
22
|
+
class SafetyConfig(BaseModel):
|
|
23
|
+
confirm_sensitive: bool = True
|
|
24
|
+
require_password_for_vault: bool = True
|
|
25
|
+
auto_lock_minutes: int = 30
|
|
26
|
+
password_min_length: int = 8
|
|
27
|
+
require_special_char: bool = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Web 配置 ─────────────────────────────────────────
|
|
31
|
+
class AuthConfig(BaseModel):
|
|
32
|
+
mode: str = "local" # local | oauth | token
|
|
33
|
+
session_expire_hours: int = 24
|
|
34
|
+
refresh_token: bool = True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WebConfig(BaseModel):
|
|
38
|
+
host: str = "127.0.0.1"
|
|
39
|
+
port: int = 8600
|
|
40
|
+
auth: AuthConfig = AuthConfig()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── 审计配置 ─────────────────────────────────────────
|
|
44
|
+
class AuditConfig(BaseModel):
|
|
45
|
+
enabled: bool = True
|
|
46
|
+
log_path: str = str(AUDIT_PATH)
|
|
47
|
+
levels: list[str] = ["SENSITIVE", "HIGH"]
|
|
48
|
+
hash_chain: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── 根配置 ───────────────────────────────────────────
|
|
52
|
+
class DevMateConfig(BaseSettings):
|
|
53
|
+
"""应用配置——从 config.yml + 环境变量加载"""
|
|
54
|
+
|
|
55
|
+
theme: str = "dark"
|
|
56
|
+
default_editor: str = os.environ.get("EDITOR", "vim")
|
|
57
|
+
db_path: str = str(DATA_PATH)
|
|
58
|
+
|
|
59
|
+
web: WebConfig = WebConfig()
|
|
60
|
+
safety: SafetyConfig = SafetyConfig()
|
|
61
|
+
audit: AuditConfig = AuditConfig()
|
|
62
|
+
|
|
63
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def load(cls) -> "DevMateConfig":
|
|
67
|
+
"""加载配置:默认值 → config.yml → 环境变量"""
|
|
68
|
+
DEVMATE_HOME.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
config = cls()
|
|
71
|
+
|
|
72
|
+
if CONFIG_PATH.exists():
|
|
73
|
+
with open(CONFIG_PATH) as f:
|
|
74
|
+
raw = yaml.safe_load(f) or {}
|
|
75
|
+
config = cls(**raw.get("devmate", {}))
|
|
76
|
+
|
|
77
|
+
# 确保配置目录和文件存在
|
|
78
|
+
_ensure_dirs()
|
|
79
|
+
_ensure_config_file(config)
|
|
80
|
+
|
|
81
|
+
return config
|
|
82
|
+
|
|
83
|
+
def save(self):
|
|
84
|
+
"""保存当前配置到文件"""
|
|
85
|
+
with open(CONFIG_PATH, "w") as f:
|
|
86
|
+
yaml.dump({"devmate": self.model_dump()}, f, default_flow_style=False)
|
|
87
|
+
|
|
88
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
89
|
+
"""按点号路径取值,如 'web.auth.mode'"""
|
|
90
|
+
parts = key.split(".")
|
|
91
|
+
value = self
|
|
92
|
+
for part in parts:
|
|
93
|
+
if isinstance(value, dict):
|
|
94
|
+
value = value.get(part)
|
|
95
|
+
else:
|
|
96
|
+
value = getattr(value, part, None)
|
|
97
|
+
if value is None:
|
|
98
|
+
return default
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _ensure_dirs():
|
|
103
|
+
"""确保数据目录存在"""
|
|
104
|
+
for d in [VAULT_PATH, EVENT_STORE_PATH]:
|
|
105
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _ensure_config_file(config: DevMateConfig):
|
|
109
|
+
"""确保配置文件存在"""
|
|
110
|
+
if not CONFIG_PATH.exists():
|
|
111
|
+
config.save()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── 全局实例 ─────────────────────────────────────────
|
|
115
|
+
config: DevMateConfig = DevMateConfig.load()
|
devmate/core/logging.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""结构化日志系统——JSON Lines 格式"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from devmate.core.config import LOG_PATH
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JsonFormatter(logging.Formatter):
|
|
14
|
+
"""JSON Lines 日志格式化器"""
|
|
15
|
+
|
|
16
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
17
|
+
log_entry: dict[str, Any] = {
|
|
18
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
19
|
+
"level": record.levelname,
|
|
20
|
+
"module": record.name,
|
|
21
|
+
"message": record.getMessage(),
|
|
22
|
+
}
|
|
23
|
+
if record.exc_info and record.exc_info[0]:
|
|
24
|
+
log_entry["exception"] = self.formatException(record.exc_info)
|
|
25
|
+
if hasattr(record, "extra_fields"):
|
|
26
|
+
log_entry.update(record.extra_fields)
|
|
27
|
+
return json.dumps(log_entry, ensure_ascii=False)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def setup_logger(name: str = "devmate", level: str = "INFO") -> logging.Logger:
|
|
31
|
+
"""初始化日志系统"""
|
|
32
|
+
logger = logging.getLogger(name)
|
|
33
|
+
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
|
34
|
+
|
|
35
|
+
# 控制台输出(人类可读)
|
|
36
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
37
|
+
console_handler.setFormatter(logging.Formatter(
|
|
38
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
39
|
+
datefmt="%H:%M:%S",
|
|
40
|
+
))
|
|
41
|
+
logger.addHandler(console_handler)
|
|
42
|
+
|
|
43
|
+
# 文件输出(JSON Lines)
|
|
44
|
+
log_path = Path(LOG_PATH)
|
|
45
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
file_handler = logging.FileHandler(log_path, encoding="utf-8")
|
|
47
|
+
file_handler.setFormatter(JsonFormatter())
|
|
48
|
+
logger.addHandler(file_handler)
|
|
49
|
+
|
|
50
|
+
return logger
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── 全局日志实例 ─────────────────────────────────────
|
|
54
|
+
logger = setup_logger()
|
devmate/core/security.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""安全基础——三级权限模型 + 敏感操作确认装饰器"""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PermissionLevel(StrEnum):
|
|
12
|
+
"""三级权限模型"""
|
|
13
|
+
SAFE = "safe"
|
|
14
|
+
SENSITIVE = "sensitive"
|
|
15
|
+
HIGH = "high"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── 敏感操作确认装饰器 ────────────────────────────────
|
|
19
|
+
|
|
20
|
+
def confirm(message: str = "确认执行此操作?", level: PermissionLevel = PermissionLevel.SENSITIVE):
|
|
21
|
+
"""敏感操作确认装饰器
|
|
22
|
+
|
|
23
|
+
用法:
|
|
24
|
+
@confirm("即将删除数据库,确认?", level=PermissionLevel.HIGH)
|
|
25
|
+
def delete_database(name: str): ...
|
|
26
|
+
"""
|
|
27
|
+
def decorator(func: Callable) -> Callable:
|
|
28
|
+
@functools.wraps(func)
|
|
29
|
+
def wrapper(*args, **kwargs):
|
|
30
|
+
if level == PermissionLevel.SENSITIVE:
|
|
31
|
+
click.confirm(message, abort=True)
|
|
32
|
+
elif level == PermissionLevel.HIGH:
|
|
33
|
+
click.confirm(f"⚠️ 高危操作:{message}", abort=True)
|
|
34
|
+
return func(*args, **kwargs)
|
|
35
|
+
return wrapper
|
|
36
|
+
return decorator
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── 敏感字段脱敏 ──────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
SENSITIVE_KEYS = {"key", "secret", "password", "token", "credential", "api_key"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def sanitize(value: Any, key: str = "") -> Any:
|
|
45
|
+
"""脱敏敏感字段值"""
|
|
46
|
+
if any(k in key.lower() for k in SENSITIVE_KEYS):
|
|
47
|
+
if isinstance(value, str) and len(value) > 8:
|
|
48
|
+
return value[:4] + "****" + value[-4:]
|
|
49
|
+
return "****"
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def sanitize_dict(data: dict) -> dict:
|
|
54
|
+
"""递归脱敏字典中的敏感字段"""
|
|
55
|
+
result = {}
|
|
56
|
+
for k, v in data.items():
|
|
57
|
+
if isinstance(v, dict):
|
|
58
|
+
result[k] = sanitize_dict(v)
|
|
59
|
+
else:
|
|
60
|
+
result[k] = sanitize(v, k)
|
|
61
|
+
return result
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Event system — Domain events, EventBus"""
|
devmate/events/bus.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
事件总线——内存版 EventBus + subscribe/emit
|
|
3
|
+
|
|
4
|
+
设计:
|
|
5
|
+
- 内存版:进程内 pub/sub,Phase 2 升级为持久化 Event Store
|
|
6
|
+
- 支持通配符订阅:on("toolkit.*") 匹配所有 toolkit 事件
|
|
7
|
+
- publish() 先写入 Event Store,再通知内存订阅者
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from devmate.core.logging import logger
|
|
15
|
+
from devmate.events.domain_event import DomainEvent
|
|
16
|
+
|
|
17
|
+
# ── 事件处理函数类型 ────────────────────────────────
|
|
18
|
+
|
|
19
|
+
EventHandler = Callable[[DomainEvent], Any]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── 事件总线 ─────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
class EventBus:
|
|
25
|
+
"""内存版事件总线——支持通配符订阅"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self._handlers: dict[str, list[EventHandler]] = {}
|
|
29
|
+
self._history: list[DomainEvent] = []
|
|
30
|
+
|
|
31
|
+
# ── 订阅 ────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
def on(self, event_pattern: str):
|
|
34
|
+
"""装饰器:订阅事件,支持通配符
|
|
35
|
+
|
|
36
|
+
用法:
|
|
37
|
+
@bus.on("toolkit.*")
|
|
38
|
+
def handler(event): ...
|
|
39
|
+
"""
|
|
40
|
+
def decorator(handler: EventHandler) -> EventHandler:
|
|
41
|
+
self.subscribe(event_pattern, handler)
|
|
42
|
+
return handler
|
|
43
|
+
return decorator
|
|
44
|
+
|
|
45
|
+
def subscribe(self, event_pattern: str, handler: EventHandler):
|
|
46
|
+
"""订阅事件(直接调用方式)"""
|
|
47
|
+
self._handlers.setdefault(event_pattern, []).append(handler)
|
|
48
|
+
|
|
49
|
+
# ── 发布 ────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
def publish(self, event: DomainEvent):
|
|
52
|
+
"""发布事件——通知所有匹配的订阅者"""
|
|
53
|
+
self._history.append(event)
|
|
54
|
+
|
|
55
|
+
for pattern, handlers in self._handlers.items():
|
|
56
|
+
if not self._matches(pattern, event.event_name):
|
|
57
|
+
continue
|
|
58
|
+
for handler in handlers:
|
|
59
|
+
try:
|
|
60
|
+
handler(event)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(f"事件处理器失败 [{pattern}]: {e}")
|
|
63
|
+
|
|
64
|
+
# ── 通配符匹配 ──────────────────────────────
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _matches(pattern: str, event_name: str) -> bool:
|
|
68
|
+
"""判断事件名是否匹配模式
|
|
69
|
+
|
|
70
|
+
支持通配符:
|
|
71
|
+
"toolkit.*" → 匹配所有 toolkit 事件
|
|
72
|
+
"*.organized" → 匹配所有 organized 事件
|
|
73
|
+
"note.created" → 精确匹配
|
|
74
|
+
"""
|
|
75
|
+
if pattern == "*":
|
|
76
|
+
return True
|
|
77
|
+
if pattern == event_name:
|
|
78
|
+
return True
|
|
79
|
+
if "*" in pattern:
|
|
80
|
+
regex = "^" + re.escape(pattern).replace("\\*", ".*") + "$"
|
|
81
|
+
return bool(re.match(regex, event_name))
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
# ── 历史 ────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
def recent(self, limit: int = 20) -> list[DomainEvent]:
|
|
87
|
+
"""最近 N 条事件"""
|
|
88
|
+
return self._history[-limit:]
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def event_count(self) -> int:
|
|
92
|
+
return len(self._history)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── 全局总线实例 ─────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
bus = EventBus()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
领域事件数据类——Event + EventMetadata
|
|
3
|
+
|
|
4
|
+
事件是模块间通信的核心载体。任何"发生了某件事"都用一个
|
|
5
|
+
DomainEvent 表示,通过 EventBus 发布给所有订阅者。
|
|
6
|
+
|
|
7
|
+
模块之间不直接 import,通过事件解耦。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class EventMetadata:
|
|
17
|
+
"""事件元数据——追踪事件的来源和链路"""
|
|
18
|
+
source_module: str = "" # 来源模块
|
|
19
|
+
source_endpoint: str = "CLI" # CLI / TUI / Web / VSCODE
|
|
20
|
+
user: str = "" # 触发用户
|
|
21
|
+
session_id: str = "" # 会话追踪 ID
|
|
22
|
+
correlation_id: str = "" # 关联 ID(跨事件追踪)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class DomainEvent:
|
|
27
|
+
"""领域事件——代表系统中发生的一件事
|
|
28
|
+
|
|
29
|
+
event_name 命名规范:<module>.<action>
|
|
30
|
+
例如:"toolkit.organized"、"db.query_executed"、"note.created"
|
|
31
|
+
"""
|
|
32
|
+
event_name: str # "toolkit.organized"
|
|
33
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
34
|
+
aggregate_type: str = "" # 聚合类型
|
|
35
|
+
aggregate_id: str = "" # 聚合 ID
|
|
36
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
37
|
+
metadata: EventMetadata = field(default_factory=EventMetadata)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Module system — Registry, Meta, Lifecycle"""
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""模块系统——ModuleMeta / ModuleManifest / ModuleRegistry"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LoadState(StrEnum):
|
|
12
|
+
PENDING = "pending"
|
|
13
|
+
LOADING = "loading"
|
|
14
|
+
ACTIVE = "active"
|
|
15
|
+
ERROR = "error"
|
|
16
|
+
DISABLED = "disabled"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ModuleMeta:
|
|
21
|
+
"""模块元数据——在导入模块之前就可用"""
|
|
22
|
+
name: str
|
|
23
|
+
version: str = "0.1.0"
|
|
24
|
+
description: str = ""
|
|
25
|
+
author: str = ""
|
|
26
|
+
dependencies: list[str] = field(default_factory=list)
|
|
27
|
+
optional_deps: list[str] = field(default_factory=list)
|
|
28
|
+
tags: list[str] = field(default_factory=list)
|
|
29
|
+
devmate_version: str = ">=0.1"
|
|
30
|
+
python_version: str = ">=3.11"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ModuleManifest:
|
|
35
|
+
"""模块运行时清单"""
|
|
36
|
+
meta: ModuleMeta
|
|
37
|
+
cli_group: Any = None # click.Group
|
|
38
|
+
load_state: LoadState = LoadState.PENDING
|
|
39
|
+
error: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ModuleRegistry:
|
|
43
|
+
"""模块注册中心——通过 entry_points 发现模块"""
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
self._modules: dict[str, ModuleManifest] = {}
|
|
47
|
+
|
|
48
|
+
def discover(self):
|
|
49
|
+
"""通过 entry_points 扫描已安装模块"""
|
|
50
|
+
for ep in importlib.metadata.entry_points(group="devmate.modules"):
|
|
51
|
+
name = ep.name
|
|
52
|
+
manifest = ModuleManifest(
|
|
53
|
+
meta=ModuleMeta(name=name, description=ep.value),
|
|
54
|
+
load_state=LoadState.PENDING,
|
|
55
|
+
)
|
|
56
|
+
self._modules[name] = manifest
|
|
57
|
+
|
|
58
|
+
def get(self, name: str) -> ModuleManifest | None:
|
|
59
|
+
return self._modules.get(name)
|
|
60
|
+
|
|
61
|
+
def list(self) -> list[ModuleManifest]:
|
|
62
|
+
return list(self._modules.values())
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def active_modules(self) -> list[ModuleManifest]:
|
|
66
|
+
return [m for m in self._modules.values() if m.load_state == LoadState.ACTIVE]
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def module_names(self) -> list[str]:
|
|
70
|
+
return list(self._modules.keys())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── 全局注册表实例 ─────────────────────────────────
|
|
74
|
+
registry = ModuleRegistry()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
audit — 审计日志模块
|
|
3
|
+
|
|
4
|
+
审计日志记录所有敏感级及以上操作,格式 JSON Lines,
|
|
5
|
+
存储到 ~/.devmate/audit.log。
|
|
6
|
+
|
|
7
|
+
每条审计记录包含:
|
|
8
|
+
- 时间戳(ISO 8601)
|
|
9
|
+
- 操作级别(SAFE / SENSITIVE / HIGH)
|
|
10
|
+
- 模块名
|
|
11
|
+
- 操作名
|
|
12
|
+
- 操作目标
|
|
13
|
+
- 执行结果
|
|
14
|
+
- 耗时
|
|
15
|
+
- 来源(CLI / TUI / Web)
|
|
16
|
+
- 用户
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from dataclasses import asdict, dataclass, field
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from devmate.core.config import AUDIT_PATH
|
|
25
|
+
|
|
26
|
+
# ── 审计条目 ──────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class AuditEntry:
|
|
30
|
+
"""单条审计记录"""
|
|
31
|
+
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
32
|
+
level: str = "SAFE" # SAFE / SENSITIVE / HIGH
|
|
33
|
+
module: str = "" # 触发模块
|
|
34
|
+
action: str = "" # 触发动作
|
|
35
|
+
target: str = "" # 操作目标
|
|
36
|
+
result: str = "success" # success / failure
|
|
37
|
+
duration_ms: float | None = None # 执行耗时
|
|
38
|
+
source: str = "CLI" # 来源终端
|
|
39
|
+
user: str = "" # 系统用户
|
|
40
|
+
detail: str = "" # 补充信息
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── 审计日志 ──────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
class AuditLog:
|
|
46
|
+
"""审计日志——JSON Lines 追加写入"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, path: str | Path = AUDIT_PATH):
|
|
49
|
+
self._path = Path(path)
|
|
50
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
def log(self, entry: AuditEntry):
|
|
53
|
+
"""写入一条审计记录"""
|
|
54
|
+
line = json.dumps(asdict(entry), ensure_ascii=False) + "\n"
|
|
55
|
+
with open(self._path, "a", encoding="utf-8") as f:
|
|
56
|
+
f.write(line)
|
|
57
|
+
|
|
58
|
+
def query(self, level: str | None = None,
|
|
59
|
+
module: str | None = None,
|
|
60
|
+
limit: int = 50) -> list[AuditEntry]:
|
|
61
|
+
"""查询审计记录,按时间倒序"""
|
|
62
|
+
if not self._path.exists():
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
results: list[AuditEntry] = []
|
|
66
|
+
with open(self._path, encoding="utf-8") as f:
|
|
67
|
+
for line in f:
|
|
68
|
+
line = line.strip()
|
|
69
|
+
if not line:
|
|
70
|
+
continue
|
|
71
|
+
data = json.loads(line)
|
|
72
|
+
entry = AuditEntry(**data)
|
|
73
|
+
|
|
74
|
+
if level and entry.level != level:
|
|
75
|
+
continue
|
|
76
|
+
if module and entry.module != module:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
results.append(entry)
|
|
80
|
+
|
|
81
|
+
# 倒序(最新的在前面)
|
|
82
|
+
results.reverse()
|
|
83
|
+
return results[:limit]
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def count(self) -> int:
|
|
87
|
+
"""审计记录总数"""
|
|
88
|
+
if not self._path.exists():
|
|
89
|
+
return 0
|
|
90
|
+
with open(self._path, encoding="utf-8") as f:
|
|
91
|
+
return sum(1 for line in f if line.strip())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── 全局实例 ──────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
audit = AuditLog()
|