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,468 @@
1
+ """资源锁模块 - v2.2.0 M2 资源锁管理
2
+
3
+ 提供文件锁、目录锁、任务锁,防止并发冲突。
4
+ """
5
+ import uuid
6
+ import time
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, timedelta
15
+ from threading import Lock as ThreadLock
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class LockType(Enum):
22
+ """锁类型枚举。"""
23
+ FILE = "file"
24
+ DIRECTORY = "directory"
25
+ TASK = "task"
26
+
27
+
28
+ class LockStatus(Enum):
29
+ """锁状态枚举。"""
30
+ ACTIVE = "active"
31
+ EXPIRED = "expired"
32
+ RELEASED = "released"
33
+ FORCE_RELEASED = "force_released"
34
+
35
+
36
+ @dataclass
37
+ class ResourceLock:
38
+ """资源锁。"""
39
+ lock_id: str
40
+ lock_type: LockType
41
+ resource_path: str
42
+ holder: str
43
+ status: LockStatus = LockStatus.ACTIVE
44
+ timeout_minutes: int = 30
45
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
46
+ expires_at: str = field(default_factory=lambda: (datetime.now() + timedelta(minutes=30)).isoformat())
47
+ reason: str = ""
48
+ force_release_reason: Optional[str] = None
49
+
50
+ def to_dict(self) -> Dict[str, Any]:
51
+ return {
52
+ "lock_id": self.lock_id,
53
+ "lock_type": self.lock_type.value,
54
+ "resource_path": self.resource_path,
55
+ "holder": self.holder,
56
+ "status": self.status.value,
57
+ "timeout_minutes": self.timeout_minutes,
58
+ "created_at": self.created_at,
59
+ "expires_at": self.expires_at,
60
+ "reason": self.reason,
61
+ "force_release_reason": self.force_release_reason
62
+ }
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: Dict[str, Any]) -> "ResourceLock":
66
+ return cls(
67
+ lock_id=data["lock_id"],
68
+ lock_type=LockType(data["lock_type"]),
69
+ resource_path=data["resource_path"],
70
+ holder=data["holder"],
71
+ status=LockStatus(data.get("status", "active")),
72
+ timeout_minutes=data.get("timeout_minutes", 30),
73
+ created_at=data.get("created_at", datetime.now().isoformat()),
74
+ expires_at=data.get("expires_at", (datetime.now() + timedelta(minutes=30)).isoformat()),
75
+ reason=data.get("reason", ""),
76
+ force_release_reason=data.get("force_release_reason")
77
+ )
78
+
79
+ def is_expired(self) -> bool:
80
+ """检查是否已过期。"""
81
+ try:
82
+ expires = datetime.fromisoformat(self.expires_at)
83
+ return datetime.now() > expires
84
+ except ValueError:
85
+ return True
86
+
87
+ def minutes_remaining(self) -> float:
88
+ """获取剩余时间(分钟)。"""
89
+ try:
90
+ expires = datetime.fromisoformat(self.expires_at)
91
+ remaining = expires - datetime.now()
92
+ return remaining.total_seconds() / 60
93
+ except ValueError:
94
+ return 0
95
+
96
+
97
+ class ResourceLockError(Exception):
98
+ """资源锁异常基类。"""
99
+ pass
100
+
101
+
102
+ class LockNotFoundError(ResourceLockError):
103
+ """锁未找到异常。"""
104
+ pass
105
+
106
+
107
+ class LockConflictError(ResourceLockError):
108
+ """锁冲突异常。"""
109
+ pass
110
+
111
+
112
+ class LockTimeoutError(ResourceLockError):
113
+ """锁超时异常。"""
114
+ pass
115
+
116
+
117
+ class PermissionDeniedError(ResourceLockError):
118
+ """权限不足异常。"""
119
+ pass
120
+
121
+
122
+ class ResourceLockManager:
123
+ """资源锁管理器。"""
124
+
125
+ DEFAULT_TIMEOUTS = {
126
+ LockType.FILE: 30,
127
+ LockType.DIRECTORY: 120,
128
+ LockType.TASK: 1440
129
+ }
130
+
131
+ def __init__(
132
+ self,
133
+ project_path: str,
134
+ locks_file: Optional[str] = None,
135
+ warning_before_minutes: int = 5
136
+ ):
137
+ """初始化资源锁管理器。
138
+
139
+ Args:
140
+ project_path: 项目路径
141
+ locks_file: 锁数据文件
142
+ warning_before_minutes: 超时前警告时间(分钟)
143
+ """
144
+ self.project_path = Path(project_path)
145
+ self.locks_file = self.project_path / (locks_file or "locks.yaml")
146
+ self.warning_before_minutes = warning_before_minutes
147
+ self.locks: Dict[str, ResourceLock] = {}
148
+ self._lock = ThreadLock()
149
+ self._load_locks()
150
+
151
+ def _load_locks(self) -> None:
152
+ """加载锁数据。"""
153
+ if self.locks_file.exists():
154
+ try:
155
+ with open(self.locks_file, 'r', encoding='utf-8') as f:
156
+ data = yaml.safe_load(f)
157
+ if data and "locks" in data:
158
+ for lock_data in data.get("locks", []):
159
+ lock = ResourceLock.from_dict(lock_data)
160
+ if lock.status == LockStatus.ACTIVE and not lock.is_expired():
161
+ self.locks[lock.lock_id] = lock
162
+ except Exception as e:
163
+ logger.warning(f"加载锁数据失败: {e}")
164
+
165
+ def _save_locks(self) -> None:
166
+ """保存锁数据。"""
167
+ data = {
168
+ "locks": [lock.to_dict() for lock in self.locks.values()],
169
+ "updated_at": datetime.now().isoformat()
170
+ }
171
+ with open(self.locks_file, 'w', encoding='utf-8') as f:
172
+ yaml.dump(data, f, allow_unicode=True)
173
+
174
+ def acquire_lock(
175
+ self,
176
+ lock_type: LockType,
177
+ resource_path: str,
178
+ holder: str,
179
+ timeout_minutes: Optional[int] = None,
180
+ reason: str = ""
181
+ ) -> ResourceLock:
182
+ """获取锁。
183
+
184
+ Args:
185
+ lock_type: 锁类型
186
+ resource_path: 资源路径
187
+ holder: 持有者
188
+ timeout_minutes: 超时时间(分钟)
189
+ reason: 锁定原因
190
+
191
+ Returns:
192
+ 创建的锁
193
+
194
+ Raises:
195
+ LockConflictError: 资源已被锁定
196
+ """
197
+ with self._lock:
198
+ existing_lock = self._find_active_lock(lock_type, resource_path)
199
+ if existing_lock:
200
+ if existing_lock.holder == holder:
201
+ return existing_lock
202
+ raise LockConflictError(
203
+ f"资源已被锁定: {resource_path} (持有者: {existing_lock.holder})"
204
+ )
205
+
206
+ timeout = timeout_minutes or self.DEFAULT_TIMEOUTS[lock_type]
207
+ lock_id = f"LOCK-{uuid.uuid4().hex[:8]}"
208
+
209
+ lock = ResourceLock(
210
+ lock_id=lock_id,
211
+ lock_type=lock_type,
212
+ resource_path=resource_path,
213
+ holder=holder,
214
+ timeout_minutes=timeout,
215
+ reason=reason
216
+ )
217
+
218
+ self.locks[lock_id] = lock
219
+ self._save_locks()
220
+ logger.info(f"获取锁: {lock_id} - {resource_path} by {holder}")
221
+ return lock
222
+
223
+ def release_lock(self, lock_id: str, holder: str) -> bool:
224
+ """释放锁。
225
+
226
+ Args:
227
+ lock_id: 锁 ID
228
+ holder: 释放者
229
+
230
+ Returns:
231
+ 是否成功释放
232
+
233
+ Raises:
234
+ LockNotFoundError: 锁未找到
235
+ PermissionDeniedError: 无权释放
236
+ """
237
+ with self._lock:
238
+ if lock_id not in self.locks:
239
+ raise LockNotFoundError(f"锁未找到: {lock_id}")
240
+
241
+ lock = self.locks[lock_id]
242
+ if lock.holder != holder:
243
+ raise PermissionDeniedError(
244
+ f"无权释放锁: {lock_id} (持有者: {lock.holder})"
245
+ )
246
+
247
+ lock.status = LockStatus.RELEASED
248
+ del self.locks[lock_id]
249
+ self._save_locks()
250
+ logger.info(f"释放锁: {lock_id}")
251
+ return True
252
+
253
+ def force_release_lock(
254
+ self,
255
+ lock_id: str,
256
+ reason: str,
257
+ requester: str
258
+ ) -> bool:
259
+ """强制释放锁(仅 Agent 1 可用)。
260
+
261
+ Args:
262
+ lock_id: 锁 ID
263
+ reason: 强制释放原因
264
+ requester: 请求者
265
+
266
+ Returns:
267
+ 是否成功释放
268
+
269
+ Raises:
270
+ LockNotFoundError: 锁未找到
271
+ """
272
+ with self._lock:
273
+ if lock_id not in self.locks:
274
+ raise LockNotFoundError(f"锁未找到: {lock_id}")
275
+
276
+ lock = self.locks[lock_id]
277
+ lock.status = LockStatus.FORCE_RELEASED
278
+ lock.force_release_reason = f"{reason} (by {requester})"
279
+ del self.locks[lock_id]
280
+ self._save_locks()
281
+ logger.info(f"强制释放锁: {lock_id} - {reason}")
282
+ return True
283
+
284
+ def get_lock(self, lock_id: str) -> ResourceLock:
285
+ """获取锁信息。
286
+
287
+ Args:
288
+ lock_id: 锁 ID
289
+
290
+ Returns:
291
+ 锁信息
292
+
293
+ Raises:
294
+ LockNotFoundError: 锁未找到
295
+ """
296
+ if lock_id not in self.locks:
297
+ raise LockNotFoundError(f"锁未找到: {lock_id}")
298
+ return self.locks[lock_id]
299
+
300
+ def get_lock_by_resource(
301
+ self,
302
+ lock_type: LockType,
303
+ resource_path: str
304
+ ) -> Optional[ResourceLock]:
305
+ """根据资源路径获取锁。
306
+
307
+ Args:
308
+ lock_type: 锁类型
309
+ resource_path: 资源路径
310
+
311
+ Returns:
312
+ 锁信息,未找到返回 None
313
+ """
314
+ return self._find_active_lock(lock_type, resource_path)
315
+
316
+ def _find_active_lock(
317
+ self,
318
+ lock_type: LockType,
319
+ resource_path: str
320
+ ) -> Optional[ResourceLock]:
321
+ """查找活跃锁。"""
322
+ for lock in self.locks.values():
323
+ if lock.lock_type == lock_type and lock.resource_path == resource_path:
324
+ if not lock.is_expired():
325
+ return lock
326
+ return None
327
+
328
+ def list_locks(
329
+ self,
330
+ holder: Optional[str] = None,
331
+ lock_type: Optional[LockType] = None,
332
+ status: Optional[LockStatus] = None
333
+ ) -> List[ResourceLock]:
334
+ """列出锁。
335
+
336
+ Args:
337
+ holder: 持有者过滤
338
+ lock_type: 锁类型过滤
339
+ status: 状态过滤
340
+
341
+ Returns:
342
+ 锁列表
343
+ """
344
+ locks = list(self.locks.values())
345
+
346
+ if holder:
347
+ locks = [l for l in locks if l.holder == holder]
348
+ if lock_type:
349
+ locks = [l for l in locks if l.lock_type == lock_type]
350
+ if status:
351
+ locks = [l for l in locks if l.status == status]
352
+
353
+ return sorted(locks, key=lambda l: l.created_at, reverse=True)
354
+
355
+ def check_timeouts(self) -> List[ResourceLock]:
356
+ """检查超时锁。
357
+
358
+ Returns:
359
+ 超时的锁列表
360
+ """
361
+ expired_locks = []
362
+ for lock in self.locks.values():
363
+ if lock.is_expired():
364
+ expired_locks.append(lock)
365
+ return expired_locks
366
+
367
+ def get_warnings(self) -> List[ResourceLock]:
368
+ """获取即将超时的锁警告。
369
+
370
+ Returns:
371
+ 即将超时的锁列表
372
+ """
373
+ warnings = []
374
+ for lock in self.locks.values():
375
+ remaining = lock.minutes_remaining()
376
+ if 0 < remaining <= self.warning_before_minutes:
377
+ warnings.append(lock)
378
+ return warnings
379
+
380
+ def auto_expire_locks(self) -> List[str]:
381
+ """自动过期处理超时的锁。
382
+
383
+ Returns:
384
+ 过期的锁 ID 列表
385
+ """
386
+ expired_ids = []
387
+ for lock_id in list(self.locks.keys()):
388
+ lock = self.locks[lock_id]
389
+ if lock.is_expired():
390
+ lock.status = LockStatus.EXPIRED
391
+ expired_ids.append(lock_id)
392
+ logger.warning(f"锁已过期自动释放: {lock_id} - {lock.resource_path}")
393
+
394
+ for lock_id in expired_ids:
395
+ del self.locks[lock_id]
396
+
397
+ if expired_ids:
398
+ self._save_locks()
399
+
400
+ return expired_ids
401
+
402
+ def get_lock_summary(self) -> Dict[str, Any]:
403
+ """获取锁管理摘要。
404
+
405
+ Returns:
406
+ 摘要信息
407
+ """
408
+ active_locks = self.list_locks(status=LockStatus.ACTIVE)
409
+ warnings = self.get_warnings()
410
+
411
+ by_type = {}
412
+ for lock_type in LockType:
413
+ by_type[lock_type.value] = len([
414
+ l for l in active_locks if l.lock_type == lock_type
415
+ ])
416
+
417
+ return {
418
+ "total_active_locks": len(active_locks),
419
+ "by_type": by_type,
420
+ "warnings": len(warnings),
421
+ "locks_file": str(self.locks_file)
422
+ }
423
+
424
+ def export_locks(self, output_path: Optional[str] = None) -> Dict[str, Any]:
425
+ """导出锁数据。
426
+
427
+ Args:
428
+ output_path: 输出路径(可选)
429
+
430
+ Returns:
431
+ 锁数据字典
432
+ """
433
+ data = {
434
+ "active_locks": [lock.to_dict() for lock in self.locks.values()],
435
+ "summary": self.get_lock_summary(),
436
+ "exported_at": datetime.now().isoformat()
437
+ }
438
+
439
+ if output_path:
440
+ with open(output_path, 'w', encoding='utf-8') as f:
441
+ yaml.dump(data, f, allow_unicode=True)
442
+
443
+ return data
444
+
445
+
446
+ if __name__ == "__main__":
447
+ import tempfile
448
+
449
+ with tempfile.TemporaryDirectory() as tmpdir:
450
+ manager = ResourceLockManager(tmpdir)
451
+
452
+ lock1 = manager.acquire_lock(
453
+ lock_type=LockType.FILE,
454
+ resource_path="src/components/Header.tsx",
455
+ holder="agent_frontend_react",
456
+ timeout_minutes=30,
457
+ reason="实现头部组件"
458
+ )
459
+ print(f"获取锁: {lock1.lock_id}")
460
+
461
+ summary = manager.get_lock_summary()
462
+ print(f"锁摘要: {summary}")
463
+
464
+ remaining = lock1.minutes_remaining()
465
+ print(f"剩余时间: {remaining:.1f} 分钟")
466
+
467
+ manager.release_lock(lock1.lock_id, "agent_frontend_react")
468
+ print(f"释放锁: {lock1.lock_id}")