auto-sync: 2026-05-18 11:34:27

This commit is contained in:
cfdaily
2026-05-18 11:34:27 +08:00
parent 1cb13c148d
commit b5136eebe8
-264
View File
@@ -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()