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.
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/METADATA +1 -1
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/RECORD +14 -6
- src/core/config_reloader.py +257 -0
- src/core/design_review_notifier.py +303 -0
- src/core/error_templates.py +208 -0
- src/core/exception_handler.py +217 -2
- src/core/git_workflow_enforcer.py +352 -0
- src/core/iteration_status_manager.py +290 -0
- src/core/monitor.py +268 -0
- src/core/state_migrator.py +404 -0
- src/core/state_validator.py +564 -0
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/WHEEL +0 -0
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/entry_points.txt +0 -0
- {opencode_collaboration-2.0.0.dist-info → opencode_collaboration-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -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/
|
|
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.
|
|
32
|
-
opencode_collaboration-2.
|
|
33
|
-
opencode_collaboration-2.
|
|
34
|
-
opencode_collaboration-2.
|
|
35
|
-
opencode_collaboration-2.
|
|
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']}")
|