diff --git a/src/blackboard/operations.py b/src/blackboard/operations.py index 691ebb1..b5ef9b7 100644 --- a/src/blackboard/operations.py +++ b/src/blackboard/operations.py @@ -598,267 +598,3 @@ class Blackboard: finally: conn.close() - -# ====================================================================== -# v2.7 Card 操作 -# ====================================================================== - -class CardOps: - """Card CRUD 操作""" - - def __init__(self, db_path: Path): - self.db_path = db_path - # 不调用 init_db,由 Blackboard 或 Ticker 负责初始化 - - def _conn(self) -> sqlite3.Connection: - return get_connection(self.db_path) - - def create_card(self, card: Card) -> Card: - """创建 Card""" - conn = self._conn() - try: - conn.execute("BEGIN IMMEDIATE") - conn.execute( - """INSERT INTO cards (id, name, description, card_type, stages_json, labels_json, status, created_at) - VALUES (?,?,?,?,?,?,?,?)""", - (card.id, card.name, card.description, card.card_type, - card.stages_json, card.labels_json, card.status, - card.created_at or datetime.utcnow().isoformat()), - ) - conn.commit() - return card - finally: - conn.close() - - def get_card(self, card_id: str) -> Optional[Card]: - """获取单个 Card""" - conn = self._conn() - try: - row = conn.execute( - "SELECT * FROM cards WHERE id=?", (card_id,) - ).fetchone() - return Card.from_row(row) if row else None - finally: - conn.close() - - def list_cards(self, status: Optional[str] = None) -> List[Card]: - """列出所有 Card""" - conn = self._conn() - try: - if status: - rows = conn.execute( - "SELECT * FROM cards WHERE status=? ORDER BY created_at ASC", - (status,), - ).fetchall() - else: - rows = conn.execute( - "SELECT * FROM cards ORDER BY created_at ASC" - ).fetchall() - return [Card.from_row(r) for r in rows] - finally: - conn.close() - - def update_card(self, card_id: str, **kwargs) -> bool: - """更新 Card 元数据""" - conn = self._conn() - try: - conn.execute("BEGIN IMMEDIATE") - existing = conn.execute( - "SELECT id FROM cards WHERE id=?", (card_id,) - ).fetchone() - if not existing: - return False - - allowed = {"name", "description", "card_type", "stages_json", - "labels_json", "status", "updated_at", "archived_at"} - updates = {k: v for k, v in kwargs.items() if k in allowed} - if not updates: - return True - - updates["updated_at"] = datetime.utcnow().isoformat() - set_clause = ", ".join(f"{k}= ?" for k in updates) - conn.execute( - f"UPDATE cards SET {set_clause} WHERE id=?", - (*updates.values(), card_id), - ) - conn.commit() - return True - finally: - conn.close() - - def delete_card(self, card_id: str) -> bool: - """删除 Card(只有 archived/deleted 状态可删)""" - conn = self._conn() - try: - conn.execute("BEGIN IMMEDIATE") - row = conn.execute( - "SELECT status FROM cards WHERE id=?", (card_id,) - ).fetchone() - if not row: - return False - if row["status"] not in ("archived", "deleted"): - return False - conn.execute("DELETE FROM cards WHERE id=?", (card_id,)) - conn.commit() - return True - finally: - conn.close() - - def fork_card(self, source_card_id: str, new_card_id: str, - new_name: str) -> Optional[Card]: - """Fork Card:复制 stages + 元数据,不复制 Task""" - source = self.get_card(source_card_id) - if not source: - return None - - new_card = Card( - id=new_card_id, - name=new_name, - description=f"Forked from {source.name} ({source_card_id})", - card_type=source.card_type, - stages_json=source.stages_json, - labels_json=source.labels_json, - status="active", - ) - return self.create_card(new_card) - - def compute_card_status(self, card_id: str) -> str: - """从 Task 聚合推导 Card 状态""" - conn = self._conn() - try: - # 检查 Card 是否有手动状态 - card = conn.execute( - "SELECT status FROM cards WHERE id=?", (card_id,) - ).fetchone() - if not card: - return "active" - - # 人工操作状态不参与推导 - if card["status"] in CARD_MANUAL_STATUSES: - return card["status"] - - # 聚合 Task 状态 - rows = conn.execute( - "SELECT status, COUNT(*) as cnt FROM tasks WHERE card_id=? " - "AND status != 'cancelled' GROUP BY status", - (card_id,), - ).fetchall() - - if not rows: - # 空 Card 或所有 Task 都 cancelled - return "active" - - status_counts = {r["status"]: r["cnt"] for r in rows} - total = sum(status_counts.values()) - done_count = status_counts.get("done", 0) - - # 所有 done → done - if done_count == total: - return "done" - - # 有 working/review/claimed → working - active_count = sum(status_counts.get(s, 0) - for s in ("working", "review", "claimed")) - if active_count > 0: - if "review" in status_counts: - return "review" - return "working" - - # 有 pending → pending - if status_counts.get("pending", 0) > 0: - return "pending" - - # 有 blocked → blocked - if status_counts.get("blocked", 0) > 0: - return "blocked" - - # 有 failed → failed - if status_counts.get("failed", 0) > 0: - return "failed" - - return "active" - finally: - conn.close() - - def refresh_card_status(self, card_id: str) -> str: - """刷新 Card 状态(从 Task 聚合推导并写入)""" - computed = self.compute_card_status(card_id) - self.update_card(card_id, status=computed) - return computed - - def card_progress(self, card_id: str) -> Dict[str, Any]: - """Card 进度信息(按 Stage 分组)""" - import json as _json - conn = self._conn() - try: - card = self.get_card(card_id) - if not card: - return {} - - stages = _json.loads(card.stages_json) - - # 总体统计 - total_row = conn.execute( - "SELECT COUNT(*) as cnt FROM tasks WHERE card_id=? AND status != 'cancelled'", - (card_id,), - ).fetchone() - done_row = conn.execute( - "SELECT COUNT(*) as cnt FROM tasks WHERE card_id=? AND status='done'", - (card_id,), - ).fetchone() - total = total_row["cnt"] if total_row else 0 - done = done_row["cnt"] if done_row else 0 - - # 按 stage 分组 - stage_progress = [] - for stage in stages: - stage_id = stage.get("id", stage.get("name", "")) - s_total = conn.execute( - "SELECT COUNT(*) as cnt FROM tasks WHERE card_id=? AND stage=? AND status != 'cancelled'", - (card_id, stage_id), - ).fetchone()["cnt"] - s_done = conn.execute( - "SELECT COUNT(*) as cnt FROM tasks WHERE card_id=? AND stage=? AND status='done'", - (card_id, stage_id), - ).fetchone()["cnt"] - s_active = conn.execute( - "SELECT COUNT(*) as cnt FROM tasks WHERE card_id=? AND stage=? AND status IN ('working','review','claimed')", - (card_id, stage_id), - ).fetchone()["cnt"] - stage_progress.append({ - "id": stage_id, - "name": stage.get("name", stage_id), - "total": s_total, - "done": s_done, - "active": s_active, - }) - - # 未分类 stage - uncategorized_total = conn.execute( - "SELECT COUNT(*) as cnt FROM tasks WHERE card_id=? AND stage IS NULL AND status != 'cancelled'", - (card_id,), - ).fetchone()["cnt"] - if uncategorized_total > 0: - uncategorized_done = conn.execute( - "SELECT COUNT(*) as cnt FROM tasks WHERE card_id=? AND stage IS NULL AND status='done'", - (card_id,), - ).fetchone()["cnt"] - stage_progress.append({ - "id": "_uncategorized", - "name": "未分类", - "total": uncategorized_total, - "done": uncategorized_done, - "active": 0, - }) - - return { - "card_id": card_id, - "card_name": card.name, - "card_status": card.status, - "total_tasks": total, - "done_tasks": done, - "progress_pct": round(done / total * 100) if total > 0 else 0, - "stages": stage_progress, - } - finally: - conn.close()