opencode-collaboration 2.0.0__py3-none-any.whl → 2.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-collaboration
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: 双Agent协作框架 - 产品经理与开发的分离式协作工具
5
5
  Author-email: OpenCode <dev@opencode.ai>
6
6
  License: MIT
@@ -10,16 +10,24 @@ src/core/auto_engine.py,sha256=bV9VXa0naPpxKuE7p7xHAZKZJe9DIGfzrFwHhiaDHKA,16031
10
10
  src/core/auto_git_sync.py,sha256=jklR_NOYBXYgRSdG0rMRgvQM3jvRBpvm-qSjc-vDK28,2077
11
11
  src/core/auto_retry.py,sha256=qG4UY2d312PoECJKKuIq3qspe6qP1mTMEQ4M2Y9J1oo,7571
12
12
  src/core/brain_engine.py,sha256=OJliBiMIxMwTCyEMJW0Os9SRpB5mCf3ujCnvjFVbLcg,14893
13
+ src/core/config_reloader.py,sha256=Fne2FJ-AgPNnqV7Ody-nRpLOSptde65ZXqyGoFl-PbE,8726
13
14
  src/core/daemon.py,sha256=pu776YVEC-dw4mKFh7JZg4q7R6XGPe52wiQtwnsQxb4,6470
15
+ src/core/design_review_notifier.py,sha256=XlWyPXUZ7V4CeWdvgf92QBqbrSvO_VzBW7VhUWmlRqk,9826
14
16
  src/core/detector.py,sha256=kSVqZ2EQXKTNByC4sOOAeIS6vdgvnwizLah13MRk54E,2328
15
17
  src/core/doc_generator.py,sha256=mwK22Pc9OJ4Mr6ZoLl1ATx4c2ZrhZLny_kCQk5mwfS4,15878
16
- src/core/exception_handler.py,sha256=nXrNsUoJJsQZa9TgavVVakhwQ7eh9wxjFuUpoucS7ZI,17395
18
+ src/core/error_templates.py,sha256=kHOTVMCGUeT53jwKYGQwPHuKZLyr7znVyzzxZfKAC6Y,7467
19
+ src/core/exception_handler.py,sha256=Yd-crydhgIRcEPnE68yXMyqDzrTRPI9_OtsICaOFbSM,24078
17
20
  src/core/git.py,sha256=guy7aE9FX4w62HQ-Wf_N3jzu_rn8JfVFLf8-Ulz-5k8,8928
18
21
  src/core/git_monitor.py,sha256=PFZxq_KodaI6zcO67-40h2vid6zdSpr3bGVZiF2fEsA,15009
22
+ src/core/git_workflow_enforcer.py,sha256=0B-xMX4T9bvyuulgqrgOlRYYwi7Vr32apjGz-9NVK00,11583
23
+ src/core/iteration_status_manager.py,sha256=ckPsJK_brsP_NSXtBLLlNrn5DqvmxvEGsE_VqjHOPJ8,9916
24
+ src/core/monitor.py,sha256=3s0xuc6l7pF4MtnRjYVxbfn8MLfKk7p7mljqtsZDQZ0,8949
19
25
  src/core/phase_advance.py,sha256=76UN8TI7RpJgRfjgV4bvs7uWlvNt-h_dz6gTDgK2zrY,11285
20
26
  src/core/signoff.py,sha256=fG6KFNz3AYh6hKYMPfognU_SBs5amCW2q4bne4huFf0,6643
21
27
  src/core/state_machine.py,sha256=L1gfdsYC6mlWcHP5RwQQen8vCTwV2spIP-Ktwz4gux8,14919
22
28
  src/core/state_manager.py,sha256=jkgci5q71DRjmKe8igeFhOTxHJ2xqNjAHJo6THFtw_M,16672
29
+ src/core/state_migrator.py,sha256=OYXtwxA_ePFAS_XqOMhKR2fyU1werLePbqCyNZg8eXQ,13746
30
+ src/core/state_validator.py,sha256=Q86jbEO0fNdzDi3zIPB_G_ibQ0QWyXFYV_rPj6Eakjw,18827
23
31
  src/core/supervisor.py,sha256=pT_5CkimpFgB_gyqzsUL-25l3MsRGP1TWFEnHdGJtwo,7290
24
32
  src/core/task_executor.py,sha256=xcM9sNu8MyAVqlNvtc2GL4eiYeTMmeduTrrA3j292aM,23720
25
33
  src/core/workflow.py,sha256=LpH9g6xbtCmYOhhCSxpcstRR7TptN8e6b0mEag3UcW4,5634
@@ -28,8 +36,8 @@ src/utils/date.py,sha256=iWS0hTaoDE2iC0jJb3lTIB5yK5xxRbrC1C98Fgb8LFc,577
28
36
  src/utils/file.py,sha256=5IFKkT2m1emJUHDzIiLsa4YG9GCqOhhmiLvc6aVY9-Y,1301
29
37
  src/utils/lock.py,sha256=soxYFsBKJHUzN-_QXkorVfgnmt0D5p1SZtqwPNqcWPI,2880
30
38
  src/utils/yaml.py,sha256=zcbh0OP7NOqxTexEAR3akQkllUh8xeKt42O2CHIImyg,777
31
- opencode_collaboration-2.0.0.dist-info/METADATA,sha256=vjjQFCc5AdQX5jBYL7zSuBV-LtJYXOYGjyNoOTmR_MA,3145
32
- opencode_collaboration-2.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
33
- opencode_collaboration-2.0.0.dist-info/entry_points.txt,sha256=fYyHWa_NefMp527B7fHl-29SwZQCElRdtxm_7LoUK-Y,48
34
- opencode_collaboration-2.0.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
35
- opencode_collaboration-2.0.0.dist-info/RECORD,,
39
+ opencode_collaboration-2.1.0.dist-info/METADATA,sha256=S0iMP93dKx2xMA5c-PkSrZ6rAW1btFTuHHOJBdLIeyI,3145
40
+ opencode_collaboration-2.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
41
+ opencode_collaboration-2.1.0.dist-info/entry_points.txt,sha256=fYyHWa_NefMp527B7fHl-29SwZQCElRdtxm_7LoUK-Y,48
42
+ opencode_collaboration-2.1.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
43
+ opencode_collaboration-2.1.0.dist-info/RECORD,,
@@ -0,0 +1,257 @@
1
+ """配置热重载模块。
2
+
3
+ 功能:
4
+ 1. 监控配置文件变化
5
+ 2. 自动重新加载配置
6
+ 3. 配置验证和回滚
7
+ """
8
+ import time
9
+ import logging
10
+ import threading
11
+ from pathlib import Path
12
+ from typing import Dict, Any, Optional, Callable
13
+ import yaml
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ConfigReloadError(Exception):
20
+ """配置重载错误。"""
21
+ pass
22
+
23
+
24
+ class ConfigValidationError(ConfigReloadError):
25
+ """配置验证错误。"""
26
+ pass
27
+
28
+
29
+ class ConfigReloader:
30
+ """配置热重载器。"""
31
+
32
+ def __init__(
33
+ self,
34
+ config_paths: Dict[str, str],
35
+ reload_callback: Callable[[str, Dict], None] = None,
36
+ validation_schema: Optional[Dict] = None
37
+ ):
38
+ """初始化。
39
+
40
+ Args:
41
+ config_paths: 配置路径映射 {name: path}
42
+ reload_callback: 重载回调函数 (config_name, config_data)
43
+ validation_schema: 配置验证 schema
44
+ """
45
+ self.config_paths = config_paths
46
+ self.reload_callback = reload_callback
47
+ self.validation_schema = validation_schema
48
+ self.configs: Dict[str, Dict] = {}
49
+ self.mtimes: Dict[str, float] = {}
50
+ self._stop_event = threading.Event()
51
+ self._monitor_thread: Optional[threading.Thread] = None
52
+ self._last_check: float = 0
53
+
54
+ def load_all(self) -> Dict[str, Dict]:
55
+ """加载所有配置文件。"""
56
+ for name, path in self.config_paths.items():
57
+ self.configs[name] = self._load_config(path)
58
+ self.mtimes[name] = Path(path).stat().st_mtime
59
+
60
+ logger.info(f"已加载 {len(self.configs)} 个配置文件: {list(self.configs.keys())}")
61
+ return self.configs
62
+
63
+ def get(self, config_name: str) -> Optional[Dict]:
64
+ """获取指定配置。"""
65
+ return self.configs.get(config_name)
66
+
67
+ def _load_config(self, path: str) -> Dict:
68
+ """加载单个配置文件。"""
69
+ try:
70
+ with open(path, 'r', encoding='utf-8') as f:
71
+ return yaml.safe_load(f) or {}
72
+ except (IOError, yaml.YAMLError) as e:
73
+ logger.error(f"加载配置文件失败: {path}, {e}")
74
+ raise ConfigReloadError(f"加载配置文件失败: {path}")
75
+
76
+ def _validate_config(self, config: Dict, config_name: str) -> bool:
77
+ """验证配置。"""
78
+ if self.validation_schema and config_name in self.validation_schema:
79
+ schema = self.validation_schema[config_name]
80
+ required_fields = schema.get("required", [])
81
+ for field in required_fields:
82
+ if field not in config:
83
+ logger.error(f"配置验证失败: 缺少必要字段 {field}")
84
+ return False
85
+ return True
86
+
87
+ def start_monitoring(self, interval: int = 60):
88
+ """开始监控配置变化。
89
+
90
+ Args:
91
+ interval: 检查间隔(秒),默认 60 秒
92
+ """
93
+ if self._monitor_thread is not None and self._monitor_thread.is_alive():
94
+ logger.warning("监控已在运行")
95
+ return
96
+
97
+ self._stop_event.clear()
98
+ self._monitor_thread = threading.Thread(
99
+ target=self._monitor_loop,
100
+ args=(interval,),
101
+ daemon=True
102
+ )
103
+ self._monitor_thread.start()
104
+ logger.info(f"配置监控已启动,间隔 {interval} 秒")
105
+
106
+ def stop_monitoring(self):
107
+ """停止监控。"""
108
+ self._stop_event.set()
109
+ if self._monitor_thread is not None:
110
+ self._monitor_thread.join(timeout=5)
111
+ self._monitor_thread = None
112
+ logger.info("配置监控已停止")
113
+
114
+ def _monitor_loop(self, interval: int):
115
+ """监控循环。"""
116
+ while not self._stop_event.is_set():
117
+ try:
118
+ self._check_changes()
119
+ except Exception as e:
120
+ logger.error(f"配置检查失败: {e}")
121
+
122
+ self._stop_event.wait(timeout=interval)
123
+
124
+ def _check_changes(self):
125
+ """检查配置变化。"""
126
+ for name, path in self.config_paths.items():
127
+ try:
128
+ current_mtime = Path(path).stat().st_mtime
129
+
130
+ if current_mtime > self.mtimes[name]:
131
+ logger.info(f"检测到配置变化: {name}")
132
+
133
+ new_config = self._load_config(path)
134
+
135
+ if self._validate_config(new_config, name):
136
+ old_config = self.configs[name]
137
+ self.configs[name] = new_config
138
+ self.mtimes[name] = current_mtime
139
+
140
+ if self.reload_callback:
141
+ self.reload_callback(name, new_config)
142
+
143
+ logger.info(f"配置已重新加载: {name}")
144
+ else:
145
+ logger.error(f"配置验证失败: {name}")
146
+
147
+ except Exception as e:
148
+ logger.error(f"检查配置变化失败 {name}: {e}")
149
+
150
+ def reload_config(self, config_name: str) -> bool:
151
+ """手动重新加载指定配置。"""
152
+ if config_name not in self.config_paths:
153
+ logger.error(f"未知配置: {config_name}")
154
+ return False
155
+
156
+ try:
157
+ path = self.config_paths[config_name]
158
+ new_config = self._load_config(path)
159
+
160
+ if self._validate_config(new_config, config_name):
161
+ self.configs[config_name] = new_config
162
+ self.mtimes[config_name] = Path(path).stat().st_mtime
163
+
164
+ if self.reload_callback:
165
+ self.reload_callback(config_name, new_config)
166
+
167
+ logger.info(f"配置已重新加载: {config_name}")
168
+ return True
169
+ else:
170
+ logger.error(f"配置验证失败: {config_name}")
171
+ return False
172
+
173
+ except Exception as e:
174
+ logger.error(f"重新加载配置失败: {config_name}, {e}")
175
+ return False
176
+
177
+ def add_config(self, name: str, path: str):
178
+ """添加新配置。"""
179
+ if name in self.config_paths:
180
+ logger.warning(f"配置已存在,将被覆盖: {name}")
181
+
182
+ self.config_paths[name] = path
183
+ config = self._load_config(path)
184
+ self.configs[name] = config
185
+ self.mtimes[name] = Path(path).stat().st_mtime
186
+ logger.info(f"已添加配置: {name}")
187
+
188
+ def remove_config(self, name: str):
189
+ """移除配置。"""
190
+ if name in self.config_paths:
191
+ del self.config_paths[name]
192
+ self.configs.pop(name, None)
193
+ self.mtimes.pop(name, None)
194
+ logger.info(f"已移除配置: {name}")
195
+
196
+ def get_status(self) -> Dict:
197
+ """获取状态。"""
198
+ return {
199
+ "monitoring": self._monitor_thread is not None and self._monitor_thread.is_alive(),
200
+ "configs": list(self.configs.keys()),
201
+ "config_count": len(self.configs)
202
+ }
203
+
204
+
205
+ class ConfigManager:
206
+ """配置管理器(简化版,无需监控)。"""
207
+
208
+ def __init__(self, config_path: str):
209
+ """初始化。
210
+
211
+ Args:
212
+ config_path: 配置文件路径
213
+ """
214
+ self.config_path = Path(config_path)
215
+ self.config = self._load()
216
+
217
+ def _load(self) -> Dict:
218
+ """加载配置。"""
219
+ if self.config_path.exists():
220
+ with open(self.config_path, 'r', encoding='utf-8') as f:
221
+ return yaml.safe_load(f) or {}
222
+ return {}
223
+
224
+ def get(self, key: str, default: Any = None) -> Any:
225
+ """获取配置值。"""
226
+ return self.config.get(key, default)
227
+
228
+ def set(self, key: str, value: Any):
229
+ """设置配置值。"""
230
+ self.config[key] = value
231
+
232
+ def save(self):
233
+ """保存配置。"""
234
+ with open(self.config_path, 'w', encoding='utf-8') as f:
235
+ yaml.dump(self.config, f, allow_unicode=True, sort_keys=False)
236
+
237
+ def reload(self):
238
+ """重新加载配置。"""
239
+ self.config = self._load()
240
+
241
+
242
+ if __name__ == "__main__":
243
+ logging.basicConfig(level=logging.INFO)
244
+
245
+ reloader = ConfigReloader(
246
+ config_paths={
247
+ "project": "state/project_state.yaml",
248
+ "settings": "config/settings.yaml"
249
+ }
250
+ )
251
+
252
+ print("加载配置:")
253
+ configs = reloader.load_all()
254
+ print(f" {list(configs.keys())}")
255
+
256
+ print("\n状态:")
257
+ print(f" {reloader.get_status()}")
@@ -0,0 +1,303 @@
1
+ """设计评审通知模块。
2
+
3
+ 功能:
4
+ 1. 评审完成后自动通知相关 Agent
5
+ 2. 需求变更时通知相关 Agent
6
+ 3. 签署完成时通知
7
+ """
8
+ import logging
9
+ from typing import Dict, List, Optional
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ from pathlib import Path
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class NotificationType(Enum):
20
+ """通知类型。"""
21
+ DESIGN_REVIEW_COMPLETE = "design_review_complete"
22
+ REQUIREMENT_CHANGED = "requirement_changed"
23
+ SIGNOFF_COMPLETE = "signoff_complete"
24
+ PHASE_ADVANCE = "phase_advance"
25
+ GENERAL = "general"
26
+
27
+
28
+ class NotificationPriority(Enum):
29
+ """通知优先级。"""
30
+ LOW = "low"
31
+ NORMAL = "normal"
32
+ HIGH = "high"
33
+ URGENT = "urgent"
34
+
35
+
36
+ @dataclass
37
+ class Notification:
38
+ """通知。"""
39
+ type: NotificationType
40
+ title: str
41
+ message: str
42
+ sender: str
43
+ recipients: List[str]
44
+ timestamp: datetime = field(default_factory=datetime.now)
45
+ priority: NotificationPriority = NotificationPriority.NORMAL
46
+ action_required: bool = False
47
+ action_url: str = ""
48
+
49
+ def to_dict(self) -> Dict:
50
+ return {
51
+ "type": self.type.value,
52
+ "title": self.title,
53
+ "message": self.message,
54
+ "sender": self.sender,
55
+ "recipients": self.recipients,
56
+ "timestamp": self.timestamp.isoformat(),
57
+ "priority": self.priority.value,
58
+ "action_required": self.action_required,
59
+ "action_url": self.action_url
60
+ }
61
+
62
+
63
+ class DesignReviewNotifier:
64
+ """设计评审通知器。"""
65
+
66
+ def __init__(self, project_path: str = "."):
67
+ """初始化。
68
+
69
+ Args:
70
+ project_path: 项目路径
71
+ """
72
+ self.project_path = Path(project_path)
73
+ self.notification_log: List[Dict] = []
74
+ self.notification_file = self.project_path / "state" / "notifications.yaml"
75
+
76
+ def _get_other_agent(self, agent_id: str) -> str:
77
+ """获取另一个 Agent ID。"""
78
+ return "agent1" if agent_id == "agent2" else "agent2"
79
+
80
+ def _send_notification(self, notification: Notification):
81
+ """发送通知。"""
82
+ self.notification_log.append(notification.to_dict())
83
+
84
+ logger.info(
85
+ f"通知已发送 - 类型: {notification.type.value}, "
86
+ f"发送给: {notification.recipients}, "
87
+ f"标题: {notification.title}"
88
+ )
89
+
90
+ self._save_notification(notification)
91
+
92
+ def _save_notification(self, notification: Notification):
93
+ """保存通知到文件。"""
94
+ notifications = self._load_notifications()
95
+ notifications.append(notification.to_dict())
96
+
97
+ try:
98
+ import yaml
99
+ with open(self.notification_file, 'w', encoding='utf-8') as f:
100
+ yaml.dump(notifications, f, allow_unicode=True, sort_keys=False)
101
+ except IOError as e:
102
+ logger.error(f"保存通知失败: {e}")
103
+
104
+ def _load_notifications(self) -> List[Dict]:
105
+ """加载通知历史。"""
106
+ if self.notification_file.exists():
107
+ try:
108
+ import yaml
109
+ with open(self.notification_file, 'r', encoding='utf-8') as f:
110
+ return yaml.safe_load(f) or []
111
+ except (IOError, yaml.YAMLError):
112
+ pass
113
+ return []
114
+
115
+ def notify_design_review_complete(self, reviewer: str, version: str):
116
+ """通知设计评审完成。
117
+
118
+ Args:
119
+ reviewer: 评审人 ID
120
+ version: 设计版本
121
+ """
122
+ notification = Notification(
123
+ type=NotificationType.DESIGN_REVIEW_COMPLETE,
124
+ title="设计评审完成",
125
+ message=f"Agent {reviewer} 已完成 v{version} 详细设计的评审。",
126
+ sender=reviewer,
127
+ recipients=[self._get_other_agent(reviewer)],
128
+ action_required=True,
129
+ action_url="docs/02-design/detailed_design_v2.1.0.md"
130
+ )
131
+
132
+ self._send_notification(notification)
133
+
134
+ def notify_requirement_changed(self, changer: str, section: str):
135
+ """通知需求变更。
136
+
137
+ Args:
138
+ changer: 变更人 ID
139
+ section: 变更的章节
140
+ """
141
+ notification = Notification(
142
+ type=NotificationType.REQUIREMENT_CHANGED,
143
+ title="需求文档更新",
144
+ message=f"Agent {changer} 更新了需求文档的 {section} 章节。",
145
+ sender=changer,
146
+ recipients=[self._get_other_agent(changer)],
147
+ action_required=True,
148
+ action_url="docs/01-requirements/requirements_v2.1.0.md"
149
+ )
150
+
151
+ self._send_notification(notification)
152
+
153
+ def notify_signoff_complete(self, signer: str, stage: str):
154
+ """通知签署完成。
155
+
156
+ Args:
157
+ signer: 签署人 ID
158
+ stage: 签署的阶段
159
+ """
160
+ notification = Notification(
161
+ type=NotificationType.SIGNOFF_COMPLETE,
162
+ title=f"{stage} 阶段签署",
163
+ message=f"Agent {signer} 已签署 {stage} 阶段。",
164
+ sender=signer,
165
+ recipients=[self._get_other_agent(signer)],
166
+ action_required=False
167
+ )
168
+
169
+ self._send_notification(notification)
170
+
171
+ def notify_phase_advance(self, actor: str, from_phase: str, to_phase: str):
172
+ """通知阶段推进。
173
+
174
+ Args:
175
+ actor: 操作人 ID
176
+ from_phase: 原始阶段
177
+ to_phase: 目标阶段
178
+ """
179
+ notification = Notification(
180
+ type=NotificationType.PHASE_ADVANCE,
181
+ title="阶段推进",
182
+ message=f"Agent {actor} 已将项目从 {from_phase} 推进到 {to_phase}。",
183
+ sender=actor,
184
+ recipients=[self._get_other_agent(actor)],
185
+ action_required=False
186
+ )
187
+
188
+ self._send_notification(notification)
189
+
190
+ def notify_general(self, title: str, message: str, sender: str, priority: NotificationPriority = NotificationPriority.NORMAL):
191
+ """发送一般通知。
192
+
193
+ Args:
194
+ title: 通知标题
195
+ message: 通知内容
196
+ sender: 发送人 ID
197
+ priority: 优先级
198
+ """
199
+ notification = Notification(
200
+ type=NotificationType.GENERAL,
201
+ title=title,
202
+ message=message,
203
+ sender=sender,
204
+ recipients=[self._get_other_agent(sender)],
205
+ priority=priority
206
+ )
207
+
208
+ self._send_notification(notification)
209
+
210
+ def get_notifications(self, agent_id: str = None, limit: int = 10) -> List[Dict]:
211
+ """获取通知历史。
212
+
213
+ Args:
214
+ agent_id: 筛选特定 Agent 的通知
215
+ limit: 返回数量限制
216
+
217
+ Returns:
218
+ 通知列表
219
+ """
220
+ notifications = self._load_notifications()
221
+
222
+ if agent_id:
223
+ notifications = [
224
+ n for n in notifications
225
+ if agent_id in n.get("recipients", []) or n.get("sender") == agent_id
226
+ ]
227
+
228
+ return notifications[-limit:]
229
+
230
+ def get_unread_count(self, agent_id: str) -> int:
231
+ """获取未读通知数量。"""
232
+ notifications = self._load_notifications()
233
+ return len([
234
+ n for n in notifications
235
+ if agent_id in n.get("recipients", []) and n.get("priority") in ["high", "urgent"]
236
+ ])
237
+
238
+ def clear_notifications(self, before_date: datetime = None) -> int:
239
+ """清理旧通知。
240
+
241
+ Args:
242
+ before_date: 清理此日期之前的通知
243
+
244
+ Returns:
245
+ 清理的通知数量
246
+ """
247
+ notifications = self._load_notifications()
248
+ cutoff = before_date or datetime.now()
249
+
250
+ original_count = len(notifications)
251
+ notifications = [
252
+ n for n in notifications
253
+ if datetime.fromisoformat(n["timestamp"]) > cutoff
254
+ ]
255
+
256
+ if len(notifications) < original_count:
257
+ try:
258
+ import yaml
259
+ with open(self.notification_file, 'w', encoding='utf-8') as f:
260
+ yaml.dump(notifications, f, allow_unicode=True, sort_keys=False)
261
+ except IOError as e:
262
+ logger.error(f"保存通知失败: {e}")
263
+
264
+ return original_count - len(notifications)
265
+
266
+ def get_notification_summary(self) -> Dict:
267
+ """获取通知摘要。"""
268
+ notifications = self._load_notifications()
269
+
270
+ by_type = {}
271
+ by_priority = {}
272
+
273
+ for n in notifications:
274
+ n_type = n.get("type", "unknown")
275
+ n_priority = n.get("priority", "normal")
276
+ by_type[n_type] = by_type.get(n_type, 0) + 1
277
+ by_priority[n_priority] = by_priority.get(n_priority, 0) + 1
278
+
279
+ return {
280
+ "total": len(notifications),
281
+ "by_type": by_type,
282
+ "by_priority": by_priority,
283
+ "action_required": len([n for n in notifications if n.get("action_required")])
284
+ }
285
+
286
+
287
+ if __name__ == "__main__":
288
+ logging.basicConfig(level=logging.INFO)
289
+
290
+ notifier = DesignReviewNotifier(".")
291
+
292
+ print("发送测试通知:")
293
+
294
+ notifier.notify_design_review_complete("agent1", "2.1.0")
295
+ notifier.notify_signoff_complete("agent2", "requirements")
296
+ notifier.notify_phase_advance("agent1", "requirements", "design")
297
+
298
+ print("\n通知摘要:")
299
+ print(notifier.get_notification_summary())
300
+
301
+ print("\n通知历史:")
302
+ for n in notifier.get_notifications():
303
+ print(f" [{n['type']}] {n['title']}")