opencode-collaboration 2.1.0__py3-none-any.whl → 2.2.0.post1__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.
@@ -0,0 +1,549 @@
1
+ """项目管理模块 - v2.2.0 M2 项目管理
2
+
3
+ 提供任务分配、依赖管理、进度可视化等功能。
4
+ """
5
+ import uuid
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Set
10
+ import yaml
11
+ import json
12
+ import logging
13
+ from datetime import datetime
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class TaskStatus(Enum):
20
+ """任务状态枚举。"""
21
+ PENDING = "pending"
22
+ IN_PROGRESS = "in_progress"
23
+ COMPLETED = "completed"
24
+ BLOCKED = "blocked"
25
+ CANCELLED = "cancelled"
26
+
27
+
28
+ class DependencyType(Enum):
29
+ """依赖类型枚举。"""
30
+ PRECEDING = "preceding" # 前置依赖
31
+ PARALLEL = "parallel" # 并行依赖
32
+ REFERENCE = "reference" # 参考依赖
33
+
34
+
35
+ @dataclass
36
+ class TaskDependency:
37
+ """任务依赖。"""
38
+ task_id: str
39
+ dependency_type: DependencyType
40
+ description: str = ""
41
+
42
+
43
+ @dataclass
44
+ class ProjectTask:
45
+ """任务。"""
46
+ task_id: str
47
+ feature_id: str
48
+ title: str
49
+ description: str = ""
50
+ assignee: Optional[str] = None
51
+ status: TaskStatus = TaskStatus.PENDING
52
+ dependencies: List[str] = field(default_factory=list)
53
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
54
+ updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
55
+ completed_at: Optional[str] = None
56
+
57
+ def to_dict(self) -> Dict[str, Any]:
58
+ return {
59
+ "task_id": self.task_id,
60
+ "feature_id": self.feature_id,
61
+ "title": self.title,
62
+ "description": self.description,
63
+ "assignee": self.assignee,
64
+ "status": self.status.value,
65
+ "dependencies": self.dependencies,
66
+ "created_at": self.created_at,
67
+ "updated_at": self.updated_at,
68
+ "completed_at": self.completed_at
69
+ }
70
+
71
+ @classmethod
72
+ def from_dict(cls, data: Dict[str, Any]) -> "ProjectTask":
73
+ return cls(
74
+ task_id=data["task_id"],
75
+ feature_id=data["feature_id"],
76
+ title=data["title"],
77
+ description=data.get("description", ""),
78
+ assignee=data.get("assignee"),
79
+ status=TaskStatus(data.get("status", "pending")),
80
+ dependencies=data.get("dependencies", []),
81
+ created_at=data.get("created_at", datetime.now().isoformat()),
82
+ updated_at=data.get("updated_at", datetime.now().isoformat()),
83
+ completed_at=data.get("completed_at")
84
+ )
85
+
86
+
87
+ @dataclass
88
+ class Feature:
89
+ """功能模块。"""
90
+ feature_id: str
91
+ title: str
92
+ description: str = ""
93
+ default_agent: Optional[str] = None
94
+ status: str = "pending"
95
+ tasks: List[ProjectTask] = field(default_factory=list)
96
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
97
+ updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
98
+
99
+ def to_dict(self) -> Dict[str, Any]:
100
+ return {
101
+ "feature_id": self.feature_id,
102
+ "title": self.title,
103
+ "description": self.description,
104
+ "default_agent": self.default_agent,
105
+ "status": self.status,
106
+ "tasks": [t.to_dict() for t in self.tasks],
107
+ "created_at": self.created_at,
108
+ "updated_at": self.updated_at
109
+ }
110
+
111
+ @classmethod
112
+ def from_dict(cls, data: Dict[str, Any]) -> "Feature":
113
+ return cls(
114
+ feature_id=data["feature_id"],
115
+ title=data["title"],
116
+ description=data.get("description", ""),
117
+ default_agent=data.get("default_agent"),
118
+ status=data.get("status", "pending"),
119
+ tasks=[ProjectTask.from_dict(t) for t in data.get("tasks", [])],
120
+ created_at=data.get("created_at", datetime.now().isoformat()),
121
+ updated_at=data.get("updated_at", datetime.now().isoformat())
122
+ )
123
+
124
+
125
+ class ProjectManagerError(Exception):
126
+ """项目管理异常基类。"""
127
+ pass
128
+
129
+
130
+ class FeatureNotFoundError(ProjectManagerError):
131
+ """Feature 未找到异常。"""
132
+ pass
133
+
134
+
135
+ class TaskNotFoundError(ProjectManagerError):
136
+ """任务未找到异常。"""
137
+ pass
138
+
139
+
140
+ class CircularDependencyError(ProjectManagerError):
141
+ """循环依赖异常。"""
142
+ pass
143
+
144
+
145
+ class ProjectManager:
146
+ """项目管理器。"""
147
+
148
+ DEFAULT_PROJECT_FILE = "project_data.yaml"
149
+
150
+ def __init__(self, project_path: str, project_file: Optional[str] = None):
151
+ """初始化项目管理器。
152
+
153
+ Args:
154
+ project_path: 项目路径
155
+ project_file: 项目数据文件
156
+ """
157
+ self.project_path = Path(project_path)
158
+ self.project_file = self.project_path / (project_file or self.DEFAULT_PROJECT_FILE)
159
+ self.features: Dict[str, Feature] = {}
160
+ self._load_project()
161
+
162
+ def _load_project(self) -> None:
163
+ """加载项目数据。"""
164
+ if self.project_file.exists():
165
+ try:
166
+ with open(self.project_file, 'r', encoding='utf-8') as f:
167
+ data = yaml.safe_load(f)
168
+ if data and "features" in data:
169
+ for feature_data in data.get("features", []):
170
+ feature = Feature.from_dict(feature_data)
171
+ self.features[feature.feature_id] = feature
172
+ except Exception as e:
173
+ logger.warning(f"加载项目数据失败: {e}")
174
+
175
+ def _save_project(self) -> None:
176
+ """保存项目数据。"""
177
+ data = {
178
+ "features": [f.to_dict() for f in self.features.values()],
179
+ "updated_at": datetime.now().isoformat()
180
+ }
181
+ with open(self.project_file, 'w', encoding='utf-8') as f:
182
+ yaml.dump(data, f, allow_unicode=True)
183
+
184
+ def create_feature(
185
+ self,
186
+ title: str,
187
+ description: str = "",
188
+ default_agent: Optional[str] = None
189
+ ) -> Feature:
190
+ """创建 Feature。
191
+
192
+ Args:
193
+ title: Feature 标题
194
+ description: 描述
195
+ default_agent: 默认负责 Agent
196
+
197
+ Returns:
198
+ 创建的 Feature
199
+ """
200
+ feature_id = f"FEATURE-{len(self.features) + 1:03d}"
201
+ feature = Feature(
202
+ feature_id=feature_id,
203
+ title=title,
204
+ description=description,
205
+ default_agent=default_agent
206
+ )
207
+ self.features[feature_id] = feature
208
+ self._save_project()
209
+ logger.info(f"创建 Feature: {feature_id} - {title}")
210
+ return feature
211
+
212
+ def get_feature(self, feature_id: str) -> Feature:
213
+ """获取 Feature。
214
+
215
+ Args:
216
+ feature_id: Feature ID
217
+
218
+ Returns:
219
+ Feature
220
+
221
+ Raises:
222
+ FeatureNotFoundError: Feature 未找到
223
+ """
224
+ if feature_id not in self.features:
225
+ raise FeatureNotFoundError(f"Feature 未找到: {feature_id}")
226
+ return self.features[feature_id]
227
+
228
+ def list_features(self, status: Optional[str] = None) -> List[Feature]:
229
+ """列出 Feature。
230
+
231
+ Args:
232
+ status: 状态过滤
233
+
234
+ Returns:
235
+ Feature 列表
236
+ """
237
+ features = list(self.features.values())
238
+ if status:
239
+ features = [f for f in features if f.status == status]
240
+ return features
241
+
242
+ def create_task(
243
+ self,
244
+ feature_id: str,
245
+ title: str,
246
+ description: str = "",
247
+ assignee: Optional[str] = None,
248
+ dependencies: Optional[List[str]] = None
249
+ ) -> ProjectTask:
250
+ """创建任务。
251
+
252
+ Args:
253
+ feature_id: 所属 Feature ID
254
+ title: 任务标题
255
+ description: 描述
256
+ assignee: 执行者
257
+ dependencies: 依赖任务 ID 列表
258
+
259
+ Returns:
260
+ 创建的任务
261
+
262
+ Raises:
263
+ FeatureNotFoundError: Feature 未找到
264
+ """
265
+ feature = self.get_feature(feature_id)
266
+
267
+ task_id = f"TASK-{len(feature.tasks) + 1:03d}"
268
+ task = ProjectTask(
269
+ task_id=task_id,
270
+ feature_id=feature_id,
271
+ title=title,
272
+ description=description,
273
+ assignee=assignee,
274
+ dependencies=dependencies or []
275
+ )
276
+
277
+ feature.tasks.append(task)
278
+ feature.updated_at = datetime.now().isoformat()
279
+ self._save_project()
280
+ logger.info(f"创建任务: {task_id} - {title}")
281
+ return task
282
+
283
+ def get_task(self, feature_id: str, task_id: str) -> ProjectTask:
284
+ """获取任务。
285
+
286
+ Args:
287
+ feature_id: Feature ID
288
+ task_id: 任务 ID
289
+
290
+ Returns:
291
+ 任务
292
+
293
+ Raises:
294
+ TaskNotFoundError: 任务未找到
295
+ """
296
+ feature = self.get_feature(feature_id)
297
+ for task in feature.tasks:
298
+ if task.task_id == task_id:
299
+ return task
300
+ raise TaskNotFoundError(f"任务未找到: {task_id}")
301
+
302
+ def list_tasks(
303
+ self,
304
+ feature_id: Optional[str] = None,
305
+ assignee: Optional[str] = None,
306
+ status: Optional[TaskStatus] = None
307
+ ) -> List[ProjectTask]:
308
+ """列出任务。
309
+
310
+ Args:
311
+ feature_id: Feature 过滤
312
+ assignee: 执行者过滤
313
+ status: 状态过滤
314
+
315
+ Returns:
316
+ 任务列表
317
+ """
318
+ tasks = []
319
+ for feature in self.features.values():
320
+ if feature_id and feature.feature_id != feature_id:
321
+ continue
322
+ for task in feature.tasks:
323
+ if assignee and task.assignee != assignee:
324
+ continue
325
+ if status and task.status != status:
326
+ continue
327
+ tasks.append(task)
328
+ return tasks
329
+
330
+ def update_task_status(
331
+ self,
332
+ feature_id: str,
333
+ task_id: str,
334
+ status: TaskStatus
335
+ ) -> ProjectTask:
336
+ """更新任务状态。
337
+
338
+ Args:
339
+ feature_id: Feature ID
340
+ task_id: 任务 ID
341
+ status: 新状态
342
+
343
+ Returns:
344
+ 更新后的任务
345
+
346
+ Raises:
347
+ TaskNotFoundError: 任务未找到
348
+ """
349
+ task = self.get_task(feature_id, task_id)
350
+ old_status = task.status
351
+ task.status = status
352
+ task.updated_at = datetime.now().isoformat()
353
+
354
+ if status == TaskStatus.COMPLETED:
355
+ task.completed_at = datetime.now().isoformat()
356
+
357
+ self._save_project()
358
+ logger.info(f"任务状态更新: {task_id} {old_status.value} -> {status.value}")
359
+ return task
360
+
361
+ def assign_task(
362
+ self,
363
+ feature_id: str,
364
+ task_id: str,
365
+ assignee: str
366
+ ) -> ProjectTask:
367
+ """分配任务。
368
+
369
+ Args:
370
+ feature_id: Feature ID
371
+ task_id: 任务 ID
372
+ assignee: 新执行者
373
+
374
+ Returns:
375
+ 更新后的任务
376
+ """
377
+ task = self.get_task(feature_id, task_id)
378
+ task.assignee = assignee
379
+ task.updated_at = datetime.now().isoformat()
380
+ self._save_project()
381
+ logger.info(f"任务分配: {task_id} -> {assignee}")
382
+ return task
383
+
384
+ def detect_circular_dependencies(self) -> List[List[str]]:
385
+ """检测循环依赖。
386
+
387
+ Returns:
388
+ 循环依赖链列表,每个链是任务 ID 列表
389
+ """
390
+ cycles = []
391
+ visited = set()
392
+ recursion_stack = set()
393
+
394
+ def dfs(task_id: str, path: List[str]) -> None:
395
+ if task_id in recursion_stack:
396
+ cycle_start = path.index(task_id)
397
+ cycles.append(path[cycle_start:] + [task_id])
398
+ return
399
+
400
+ if task_id in visited:
401
+ return
402
+
403
+ visited.add(task_id)
404
+ recursion_stack.add(task_id)
405
+ path.append(task_id)
406
+
407
+ for feature in self.features.values():
408
+ for task in feature.tasks:
409
+ if task.task_id != task_id:
410
+ continue
411
+ for dep_id in task.dependencies:
412
+ if dep_id not in visited:
413
+ dfs(dep_id, path.copy())
414
+
415
+ recursion_stack.remove(task_id)
416
+ path.pop()
417
+
418
+ for feature in self.features.values():
419
+ for task in feature.tasks:
420
+ if task.task_id not in visited:
421
+ dfs(task.task_id, [])
422
+
423
+ return cycles
424
+
425
+ def check_dependencies_resolved(
426
+ self,
427
+ feature_id: str,
428
+ task_id: str
429
+ ) -> tuple[bool, List[str]]:
430
+ """检查任务依赖是否已解决。
431
+
432
+ Args:
433
+ feature_id: Feature ID
434
+ task_id: 任务 ID
435
+
436
+ Returns:
437
+ (是否可开始, 未解决的依赖列表)
438
+ """
439
+ task = self.get_task(feature_id, task_id)
440
+ unresolved = []
441
+
442
+ for dep_id in task.dependencies:
443
+ dep_task = None
444
+ for feature in self.features.values():
445
+ for t in feature.tasks:
446
+ if t.task_id == dep_id:
447
+ dep_task = t
448
+ break
449
+ if dep_task:
450
+ break
451
+
452
+ if not dep_task or dep_task.status != TaskStatus.COMPLETED:
453
+ unresolved.append(dep_id)
454
+
455
+ return len(unresolved) == 0, unresolved
456
+
457
+ def get_progress_summary(self) -> Dict[str, Any]:
458
+ """获取进度摘要。
459
+
460
+ Returns:
461
+ 进度摘要
462
+ """
463
+ total_tasks = 0
464
+ completed_tasks = 0
465
+ in_progress_tasks = 0
466
+ blocked_tasks = 0
467
+ by_assignee = {}
468
+
469
+ for feature in self.features.values():
470
+ for task in feature.tasks:
471
+ total_tasks += 1
472
+ if task.status == TaskStatus.COMPLETED:
473
+ completed_tasks += 1
474
+ elif task.status == TaskStatus.IN_PROGRESS:
475
+ in_progress_tasks += 1
476
+ elif task.status == TaskStatus.BLOCKED:
477
+ blocked_tasks += 1
478
+
479
+ if task.assignee:
480
+ if task.assignee not in by_assignee:
481
+ by_assignee[task.assignee] = {"total": 0, "completed": 0, "in_progress": 0}
482
+ by_assignee[task.assignee]["total"] += 1
483
+ if task.status == TaskStatus.COMPLETED:
484
+ by_assignee[task.assignee]["completed"] += 1
485
+ elif task.status == TaskStatus.IN_PROGRESS:
486
+ by_assignee[task.assignee]["in_progress"] += 1
487
+
488
+ return {
489
+ "total_features": len(self.features),
490
+ "total_tasks": total_tasks,
491
+ "completed_tasks": completed_tasks,
492
+ "in_progress_tasks": in_progress_tasks,
493
+ "blocked_tasks": blocked_tasks,
494
+ "completion_rate": round(completed_tasks / total_tasks * 100, 2) if total_tasks > 0 else 0,
495
+ "by_assignee": by_assignee
496
+ }
497
+
498
+ def export_project(self, output_path: Optional[str] = None) -> Dict[str, Any]:
499
+ """导出项目数据。
500
+
501
+ Args:
502
+ output_path: 输出路径(可选)
503
+
504
+ Returns:
505
+ 项目数据字典
506
+ """
507
+ data = {
508
+ "features": [f.to_dict() for f in self.features.values()],
509
+ "progress": self.get_progress_summary(),
510
+ "exported_at": datetime.now().isoformat()
511
+ }
512
+
513
+ if output_path:
514
+ with open(output_path, 'w', encoding='utf-8') as f:
515
+ yaml.dump(data, f, allow_unicode=True)
516
+
517
+ return data
518
+
519
+
520
+ if __name__ == "__main__":
521
+ import tempfile
522
+
523
+ with tempfile.TemporaryDirectory() as tmpdir:
524
+ pm = ProjectManager(tmpdir)
525
+
526
+ feature = pm.create_feature(
527
+ title="用户认证模块",
528
+ description="用户登录、注册、登出功能",
529
+ default_agent="agent_backend_go"
530
+ )
531
+ print(f"创建 Feature: {feature.feature_id}")
532
+
533
+ task1 = pm.create_task(
534
+ feature_id=feature.feature_id,
535
+ title="设计用户认证 API",
536
+ assignee="agent_backend_go"
537
+ )
538
+ print(f"创建任务: {task1.task_id}")
539
+
540
+ task2 = pm.create_task(
541
+ feature_id=feature.feature_id,
542
+ title="实现 JWT 认证",
543
+ assignee="agent_backend_go",
544
+ dependencies=[task1.task_id]
545
+ )
546
+ print(f"创建任务: {task2.task_id}")
547
+
548
+ progress = pm.get_progress_summary()
549
+ print(f"进度: {progress}")