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,502 @@
1
+ """会议管理模块 - v2.2.0 M3 会议管理
2
+
3
+ 提供会议创建、导入、列表、详情、纪要生成等功能。
4
+ """
5
+ import uuid
6
+ import os
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+ import yaml
12
+ import json
13
+ import logging
14
+ from datetime import datetime
15
+
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class MeetingType(Enum):
21
+ """会议类型枚举。"""
22
+ AGENT_DISCUSSION = "agent_discussion" # Agent 间讨论
23
+ CUSTOMER_MEETING = "customer_meeting" # 产品经理与客户讨论
24
+
25
+
26
+ class MeetingStatus(Enum):
27
+ """会议状态枚举。"""
28
+ DRAFT = "draft" # 草稿
29
+ IN_PROGRESS = "in_progress" # 进行中
30
+ COMPLETED = "completed" # 已完成
31
+ ARCHIVED = "archived" # 已归档
32
+
33
+
34
+ @dataclass
35
+ class MeetingAttachment:
36
+ """会议附件。"""
37
+ filename: str
38
+ file_path: str
39
+ file_type: str # mp3, wav, txt, pdf, etc.
40
+ upload_time: str = field(default_factory=lambda: datetime.now().isoformat())
41
+ size: int = 0
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ return {
45
+ "filename": self.filename,
46
+ "file_path": self.file_path,
47
+ "file_type": self.file_type,
48
+ "upload_time": self.upload_time,
49
+ "size": self.size
50
+ }
51
+
52
+
53
+ @dataclass
54
+ class Meeting:
55
+ """会议。"""
56
+ meeting_id: str
57
+ title: str
58
+ meeting_type: MeetingType
59
+ version: str
60
+ participants: List[str] = field(default_factory=list)
61
+ date: str = field(default_factory=lambda: datetime.now().isoformat())
62
+ decisions: List[str] = field(default_factory=list)
63
+ action_items: List[str] = field(default_factory=list)
64
+ attachments: List[MeetingAttachment] = field(default_factory=list)
65
+ status: MeetingStatus = MeetingStatus.DRAFT
66
+ summary: Optional[str] = None
67
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
68
+ updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
69
+
70
+ def to_dict(self) -> Dict[str, Any]:
71
+ return {
72
+ "meeting_id": self.meeting_id,
73
+ "title": self.title,
74
+ "meeting_type": self.meeting_type.value,
75
+ "version": self.version,
76
+ "participants": self.participants,
77
+ "date": self.date,
78
+ "decisions": self.decisions,
79
+ "action_items": self.action_items,
80
+ "attachments": [a.to_dict() for a in self.attachments],
81
+ "status": self.status.value,
82
+ "summary": self.summary,
83
+ "created_at": self.created_at,
84
+ "updated_at": self.updated_at
85
+ }
86
+
87
+ @classmethod
88
+ def from_dict(cls, data: Dict[str, Any]) -> "Meeting":
89
+ return cls(
90
+ meeting_id=data["meeting_id"],
91
+ title=data["title"],
92
+ meeting_type=MeetingType(data["meeting_type"]),
93
+ version=data["version"],
94
+ participants=data.get("participants", []),
95
+ date=data.get("date", datetime.now().isoformat()),
96
+ decisions=data.get("decisions", []),
97
+ action_items=data.get("action_items", []),
98
+ attachments=[
99
+ MeetingAttachment(**a) for a in data.get("attachments", [])
100
+ ],
101
+ status=MeetingStatus(data.get("status", "draft")),
102
+ summary=data.get("summary"),
103
+ created_at=data.get("created_at", datetime.now().isoformat()),
104
+ updated_at=data.get("updated_at", datetime.now().isoformat())
105
+ )
106
+
107
+
108
+ class MeetingManagerError(Exception):
109
+ """会议管理异常基类。"""
110
+ pass
111
+
112
+
113
+ class MeetingNotFoundError(MeetingManagerError):
114
+ """会议未找到异常。"""
115
+ pass
116
+
117
+
118
+ class MeetingManager:
119
+ """会议管理器。"""
120
+
121
+ DEFAULT_MEETINGS_DIR = "meetings"
122
+ MEETINGS_FILE = "meetings.yaml"
123
+
124
+ def __init__(
125
+ self,
126
+ project_path: str,
127
+ meetings_dir: Optional[str] = None,
128
+ meetings_file: Optional[str] = None
129
+ ):
130
+ """初始化会议管理器。
131
+
132
+ Args:
133
+ project_path: 项目路径
134
+ meetings_dir: 会议附件目录
135
+ meetings_file: 会议数据文件
136
+ """
137
+ self.project_path = Path(project_path)
138
+ self.meetings_dir = self.project_path / (meetings_dir or self.DEFAULT_MEETINGS_DIR)
139
+ self.meetings_file = self.project_path / (meetings_file or self.MEETINGS_FILE)
140
+ self.meetings: Dict[str, Meeting] = {}
141
+ self._ensure_directories()
142
+ self._load_meetings()
143
+
144
+ def _ensure_directories(self) -> None:
145
+ """确保目录存在。"""
146
+ self.meetings_dir.mkdir(parents=True, exist_ok=True)
147
+
148
+ def _load_meetings(self) -> None:
149
+ """加载会议数据。"""
150
+ if self.meetings_file.exists():
151
+ try:
152
+ with open(self.meetings_file, 'r', encoding='utf-8') as f:
153
+ data = yaml.safe_load(f)
154
+ if data and "meetings" in data:
155
+ for meeting_data in data.get("meetings", []):
156
+ meeting = Meeting.from_dict(meeting_data)
157
+ self.meetings[meeting.meeting_id] = meeting
158
+ except Exception as e:
159
+ logger.warning(f"加载会议数据失败: {e}")
160
+
161
+ def _save_meetings(self) -> None:
162
+ """保存会议数据。"""
163
+ data = {
164
+ "meetings": [m.to_dict() for m in self.meetings.values()],
165
+ "updated_at": datetime.now().isoformat()
166
+ }
167
+ with open(self.meetings_file, 'w', encoding='utf-8') as f:
168
+ yaml.dump(data, f, allow_unicode=True)
169
+
170
+ def create_meeting(
171
+ self,
172
+ title: str,
173
+ meeting_type: MeetingType,
174
+ version: str,
175
+ participants: Optional[List[str]] = None,
176
+ date: Optional[str] = None
177
+ ) -> Meeting:
178
+ """创建会议。
179
+
180
+ Args:
181
+ title: 会议标题
182
+ meeting_type: 会议类型
183
+ version: 关联版本
184
+ participants: 参与者
185
+ date: 会议日期
186
+
187
+ Returns:
188
+ 创建的会议
189
+ """
190
+ meeting_id = f"MTG-{len(self.meetings) + 1:03d}"
191
+ meeting = Meeting(
192
+ meeting_id=meeting_id,
193
+ title=title,
194
+ meeting_type=meeting_type,
195
+ version=version,
196
+ participants=participants or [],
197
+ date=date or datetime.now().isoformat()
198
+ )
199
+ self.meetings[meeting_id] = meeting
200
+ self._save_meetings()
201
+ logger.info(f"创建会议: {meeting_id} - {title}")
202
+ return meeting
203
+
204
+ def get_meeting(self, meeting_id: str) -> Meeting:
205
+ """获取会议。
206
+
207
+ Args:
208
+ meeting_id: 会议 ID
209
+
210
+ Returns:
211
+ 会议
212
+
213
+ Raises:
214
+ MeetingNotFoundError: 会议未找到
215
+ """
216
+ if meeting_id not in self.meetings:
217
+ raise MeetingNotFoundError(f"会议未找到: {meeting_id}")
218
+ return self.meetings[meeting_id]
219
+
220
+ def list_meetings(
221
+ self,
222
+ version: Optional[str] = None,
223
+ status: Optional[MeetingStatus] = None,
224
+ meeting_type: Optional[MeetingType] = None
225
+ ) -> List[Meeting]:
226
+ """列出会议。
227
+
228
+ Args:
229
+ version: 版本过滤
230
+ status: 状态过滤
231
+ meeting_type: 类型过滤
232
+
233
+ Returns:
234
+ 会议列表
235
+ """
236
+ meetings = list(self.meetings.values())
237
+
238
+ if version:
239
+ meetings = [m for m in meetings if m.version == version]
240
+ if status:
241
+ meetings = [m for m in meetings if m.status == status]
242
+ if meeting_type:
243
+ meetings = [m for m in meetings if m.meeting_type == meeting_type]
244
+
245
+ return sorted(meetings, key=lambda m: m.date, reverse=True)
246
+
247
+ def add_decision(self, meeting_id: str, decision: str) -> Meeting:
248
+ """添加决策。
249
+
250
+ Args:
251
+ meeting_id: 会议 ID
252
+ decision: 决策内容
253
+
254
+ Returns:
255
+ 更新后的会议
256
+
257
+ Raises:
258
+ MeetingNotFoundError: 会议未找到
259
+ """
260
+ meeting = self.get_meeting(meeting_id)
261
+ meeting.decisions.append(decision)
262
+ meeting.updated_at = datetime.now().isoformat()
263
+ self._save_meetings()
264
+ logger.info(f"添加决策: {meeting_id} - {decision[:50]}...")
265
+ return meeting
266
+
267
+ def add_action_item(self, meeting_id: str, action_item: str) -> Meeting:
268
+ """添加待办事项。
269
+
270
+ Args:
271
+ meeting_id: 会议 ID
272
+ action_item: 待办事项
273
+
274
+ Returns:
275
+ 更新后的会议
276
+
277
+ Raises:
278
+ MeetingNotFoundError: 会议未找到
279
+ """
280
+ meeting = self.get_meeting(meeting_id)
281
+ meeting.action_items.append(action_item)
282
+ meeting.updated_at = datetime.now().isoformat()
283
+ self._save_meetings()
284
+ logger.info(f"添加待办: {meeting_id} - {action_item[:50]}...")
285
+ return meeting
286
+
287
+ def upload_attachment(
288
+ self,
289
+ meeting_id: str,
290
+ file_path: str,
291
+ target_dir: Optional[str] = None
292
+ ) -> MeetingAttachment:
293
+ """上传附件。
294
+
295
+ Args:
296
+ meeting_id: 会议 ID
297
+ file_path: 源文件路径
298
+ target_dir: 目标目录
299
+
300
+ Returns:
301
+ 创建的附件
302
+
303
+ Raises:
304
+ MeetingNotFoundError: 会议未找到
305
+ """
306
+ meeting = self.get_meeting(meeting_id)
307
+
308
+ source_path = Path(file_path)
309
+ if not source_path.exists():
310
+ raise FileNotFoundError(f"文件不存在: {file_path}")
311
+
312
+ target_directory = self.meetings_dir / meeting_id / (target_dir or "attachments")
313
+ target_directory.mkdir(parents=True, exist_ok=True)
314
+
315
+ target_path = target_directory / source_path.name
316
+
317
+ import shutil
318
+ shutil.copy2(source_path, target_path)
319
+
320
+ file_type = source_path.suffix.lower().lstrip(".")
321
+ attachment = MeetingAttachment(
322
+ filename=source_path.name,
323
+ file_path=str(target_path),
324
+ file_type=file_type,
325
+ size=target_path.stat().st_size
326
+ )
327
+
328
+ meeting.attachments.append(attachment)
329
+ meeting.updated_at = datetime.now().isoformat()
330
+ self._save_meetings()
331
+
332
+ logger.info(f"上传附件: {meeting_id} - {source_path.name}")
333
+ return attachment
334
+
335
+ def generate_summary(self, meeting_id: str) -> str:
336
+ """生成会议纪要。
337
+
338
+ Args:
339
+ meeting_id: 会议 ID
340
+
341
+ Returns:
342
+ 会议纪要内容
343
+
344
+ Raises:
345
+ MeetingNotFoundError: 会议未找到
346
+ """
347
+ meeting = self.get_meeting(meeting_id)
348
+
349
+ summary_parts = [
350
+ f"# {meeting.title}",
351
+ "",
352
+ f"**会议编号**: {meeting.meeting_id}",
353
+ f"**日期**: {meeting.date}",
354
+ f"**参与者**: {', '.join(meeting.participants) if meeting.participants else '无'}",
355
+ f"**版本**: {meeting.version}",
356
+ f"**状态**: {meeting.status.value}",
357
+ "",
358
+ "---",
359
+ "",
360
+ "## 关键决策",
361
+ ""
362
+ ]
363
+
364
+ for decision in meeting.decisions:
365
+ summary_parts.append(f"- {decision}")
366
+
367
+ summary_parts.extend([
368
+ "",
369
+ "## 待办事项",
370
+ ""
371
+ ])
372
+
373
+ for item in meeting.action_items:
374
+ summary_parts.append(f"- [ ] {item}")
375
+
376
+ if meeting.attachments:
377
+ summary_parts.extend([
378
+ "",
379
+ "## 附件",
380
+ ""
381
+ ])
382
+ for attachment in meeting.attachments:
383
+ summary_parts.append(f"- [{attachment.filename}]({attachment.file_path})")
384
+
385
+ summary = "\n".join(summary_parts)
386
+ meeting.summary = summary
387
+ meeting.status = MeetingStatus.COMPLETED
388
+ meeting.updated_at = datetime.now().isoformat()
389
+ self._save_meetings()
390
+
391
+ logger.info(f"生成会议纪要: {meeting_id}")
392
+ return summary
393
+
394
+ def update_meeting_status(
395
+ self,
396
+ meeting_id: str,
397
+ status: MeetingStatus
398
+ ) -> Meeting:
399
+ """更新会议状态。
400
+
401
+ Args:
402
+ meeting_id: 会议 ID
403
+ status: 新状态
404
+
405
+ Returns:
406
+ 更新后的会议
407
+
408
+ Raises:
409
+ MeetingNotFoundError: 会议未找到
410
+ """
411
+ meeting = self.get_meeting(meeting_id)
412
+ meeting.status = status
413
+ meeting.updated_at = datetime.now().isoformat()
414
+ self._save_meetings()
415
+ logger.info(f"更新状态: {meeting_id} -> {status.value}")
416
+ return meeting
417
+
418
+ def get_meetings_by_version(self, version: str) -> List[Meeting]:
419
+ """获取指定版本的所有会议。
420
+
421
+ Args:
422
+ version: 版本号
423
+
424
+ Returns:
425
+ 会议列表
426
+ """
427
+ return self.list_meetings(version=version)
428
+
429
+ def get_meeting_summary(self) -> Dict[str, Any]:
430
+ """获取会议管理摘要。
431
+
432
+ Returns:
433
+ 摘要信息
434
+ """
435
+ by_status = {}
436
+ by_version = {}
437
+ by_type = {}
438
+
439
+ for meeting in self.meetings.values():
440
+ by_status[meeting.status.value] = by_status.get(meeting.status.value, 0) + 1
441
+ by_version[meeting.version] = by_version.get(meeting.version, 0) + 1
442
+ by_type[meeting.meeting_type.value] = by_type.get(meeting.meeting_type.value, 0) + 1
443
+
444
+ return {
445
+ "total_meetings": len(self.meetings),
446
+ "by_status": by_status,
447
+ "by_version": by_version,
448
+ "by_type": by_type,
449
+ "meetings_dir": str(self.meetings_dir)
450
+ }
451
+
452
+ def export_meetings(self, output_path: Optional[str] = None) -> Dict[str, Any]:
453
+ """导出会议数据。
454
+
455
+ Args:
456
+ output_path: 输出路径(可选)
457
+
458
+ Returns:
459
+ 会议数据字典
460
+ """
461
+ data = {
462
+ "meetings": [m.to_dict() for m in self.meetings.values()],
463
+ "summary": self.get_meeting_summary(),
464
+ "exported_at": datetime.now().isoformat()
465
+ }
466
+
467
+ if output_path:
468
+ with open(output_path, 'w', encoding='utf-8') as f:
469
+ yaml.dump(data, f, allow_unicode=True)
470
+
471
+ return data
472
+
473
+
474
+ if __name__ == "__main__":
475
+ import tempfile
476
+
477
+ with tempfile.TemporaryDirectory() as tmpdir:
478
+ manager = MeetingManager(tmpdir)
479
+
480
+ meeting = manager.create_meeting(
481
+ title="v2.2.0 需求讨论",
482
+ meeting_type=MeetingType.AGENT_DISCUSSION,
483
+ version="v2.2.0",
484
+ participants=["Agent 1", "Agent 2"]
485
+ )
486
+ print(f"创建会议: {meeting.meeting_id}")
487
+
488
+ manager.add_decision(
489
+ meeting.meeting_id,
490
+ "资源锁超时采用分层通知机制"
491
+ )
492
+
493
+ manager.add_action_item(
494
+ meeting.meeting_id,
495
+ "创建概要设计文档"
496
+ )
497
+
498
+ summary = manager.generate_summary(meeting.meeting_id)
499
+ print(f"\n会议纪要:\n{summary}")
500
+
501
+ summary_data = manager.get_meeting_summary()
502
+ print(f"\n摘要: {summary_data}")