ForcePilot/backend/package/yuxi/channels/adapters/urbit/approval.py
Kris 1f78c44b03 refactor: 整理并清理项目中的冗余代码与格式问题
这是一个批量整理提交,包含以下主要改动:
1.  删除多处冗余的空行和未使用的导入
2.  修复文件末尾缺少换行符的问题
3.  调整部分模块的导入顺序与代码排版
4.  修复部分配置默认值与策略逻辑
5.  新增多个功能模块与辅助工具
6.  完善异常处理与日志记录
7.  修复速率限制、消息缓存、权限校验等逻辑bug
8.  废弃部分旧有API与配置项并添加警告提示
2026-05-12 14:51:53 +08:00

270 lines
9.9 KiB
Python

from __future__ import annotations
import time
import uuid
from dataclasses import dataclass
from typing import Any
from yuxi.utils.logging_config import logger
@dataclass
class PendingApproval:
id: str
approval_type: str
requester_ship: str
channel_id: str
message_content: str
created_at: float
status: str = "pending"
def approve(self) -> None:
self.status = "approved"
def deny(self) -> None:
self.status = "denied"
class ApprovalSystem:
def __init__(self, owner_ship: str = ""):
self._owner_ship = owner_ship.lstrip("~")
self._pending: dict[str, PendingApproval] = {}
self._resolved: dict[str, PendingApproval] = {}
self._blocked_ships: set[str] = set()
@property
def owner_ship(self) -> str:
return self._owner_ship
def create_pending(
self,
approval_type: str,
requester_ship: str,
channel_id: str,
message_content: str,
) -> PendingApproval:
approval = PendingApproval(
id=f"{approval_type}-{int(time.time())}-{str(uuid.uuid4())[:8]}",
approval_type=approval_type,
requester_ship=requester_ship,
channel_id=channel_id,
message_content=message_content,
created_at=time.time(),
)
self._pending[approval.id] = approval
logger.info(f"[Urbit] Approval {approval.id} created: {approval_type} from ~{requester_ship}")
return approval
def approve(self, approval_id: str) -> PendingApproval | None:
approval = self._pending.pop(approval_id, None)
if approval:
approval.approve()
self._resolved[approval_id] = approval
logger.info(f"[Urbit] Approval {approval_id} approved")
return approval
def deny(self, approval_id: str) -> PendingApproval | None:
approval = self._pending.pop(approval_id, None)
if approval:
approval.deny()
self._resolved[approval_id] = approval
logger.info(f"[Urbit] Approval {approval_id} denied")
return approval
def get_pending(self) -> list[PendingApproval]:
return list(self._pending.values())
def get_pending_count(self) -> int:
return len(self._pending)
def block_ship(self, ship: str) -> bool:
ship = ship.lstrip("~").lower()
if ship in self._blocked_ships:
return False
self._blocked_ships.add(ship)
logger.info(f"[Urbit] Ship blocked: ~{ship}")
return True
def unblock_ship(self, ship: str) -> bool:
ship = ship.lstrip("~").lower()
if ship not in self._blocked_ships:
return False
self._blocked_ships.discard(ship)
logger.info(f"[Urbit] Ship unblocked: ~{ship}")
return True
def get_blocked_ships(self) -> list[str]:
return sorted(self._blocked_ships)
def is_ship_blocked(self, ship: str) -> bool:
return ship.lstrip("~").lower() in self._blocked_ships
def has_approved(self, ship: str) -> bool:
normalized = ship.lstrip("~").lower()
for approval in self._pending.values():
if approval.requester_ship.lstrip("~").lower() == normalized and approval.status == "approved":
return True
for approval in self._resolved.values():
if approval.requester_ship.lstrip("~").lower() == normalized and approval.status == "approved":
return True
return False
def has_duplicate_pending(self, approval_type: str, requester_ship: str, channel_id: str) -> bool:
req_ship = requester_ship.lstrip("~").lower()
for p in self._pending.values():
if (
p.approval_type == approval_type
and p.requester_ship.lstrip("~").lower() == req_ship
and p.channel_id == channel_id
and p.status == "pending"
):
return True
return False
def build_owner_notification(self, approval: PendingApproval) -> str:
return (
f"New {approval.approval_type} request from ~{approval.requester_ship}"
f" (ID: {approval.id}): {approval.message_content[:200]}"
)
def build_approval_instructions(self, approval_id: str) -> str:
return (
f"To approve: send `approve {approval_id}`\n"
f"To deny: send `deny {approval_id}`\n"
f"To block: send `block {approval_id}`\n"
f"To list pending: send `pending`\n"
f"To list blocked: send `blocked`"
)
def is_approval_command(self, content: str) -> bool:
return any(
content.strip().startswith(cmd)
for cmd in ("approve ", "deny ", "block ", "unblock ", "approvals", "pending", "blocked")
)
def handle_admin_command(self, content: str, sender_ship: str) -> str | None:
if not self._owner_ship or sender_ship.lower() != self._owner_ship.lower():
return None
content = content.strip()
if content.startswith("approve "):
aid = content.split(" ", 1)[1].strip()
approval = self.approve(aid)
if approval:
return f"Approved {aid} (~{approval.requester_ship})"
return f"Approval {aid} not found"
if content.startswith("deny "):
aid = content.split(" ", 1)[1].strip()
approval = self.deny(aid)
if approval:
return f"Denied {aid} (~{approval.requester_ship})"
return f"Approval {aid} not found"
if content.startswith("block "):
aid = content.split(" ", 1)[1].strip()
approval = self._pending.get(aid)
if approval:
self.block_ship(approval.requester_ship)
self.deny(aid)
return f"Blocked and denied {aid} (~{approval.requester_ship})"
return f"Approval {aid} not found"
if content.startswith("unblock "):
ship = content.split(" ", 1)[1].strip()
if self.unblock_ship(ship):
return f"Unblocked ~{ship}"
return f"Ship ~{ship} is not blocked"
if content in ("approvals", "pending"):
pending = self.get_pending()
if not pending:
return "No pending approvals"
lines = ["Pending approvals:"]
for p in pending:
lines.append(f" {p.id}: {p.approval_type} from ~{p.requester_ship}")
return "\n".join(lines)
if content == "blocked":
blocked = self.get_blocked_ships()
if not blocked:
return "No blocked ships"
lines = ["Blocked ships:"]
for ship in blocked:
lines.append(f" ~{ship}")
return "\n".join(lines)
return None
async def persist_to_settings_store(self, store: SettingsStore) -> None:
from .settings import SettingsStore
store_data: dict[str, Any] = {
"pending_approvals": [],
"blocked_ships": sorted(self._blocked_ships),
}
for p in self._pending.values():
store_data["pending_approvals"].append(
{
"id": p.id,
"type": p.approval_type,
"requester": p.requester_ship,
"channel": p.channel_id,
"content": p.message_content[:500],
"created_at": p.created_at,
"status": p.status,
}
)
try:
ok_pending = store.merge_key("pendingApprovals", store_data["pending_approvals"])
ok_blocked = store.merge_key("blockedShips", store_data["blocked_ships"])
if not ok_pending or not ok_blocked:
logger.warning("[Urbit] Settings Store merge conflict, retrying...")
await store.reload()
store.merge_key("pendingApprovals", store_data["pending_approvals"])
store.merge_key("blockedShips", store_data["blocked_ships"])
logger.debug(
f"[Urbit] Persisted {len(self._pending)} pending approvals "
f"and {len(self._blocked_ships)} blocked ships to Settings Store"
)
except Exception as e:
logger.warning(f"[Urbit] Failed to persist approvals: {e}")
async def load_from_settings_store(self, client: Any) -> list[PendingApproval]:
from .settings import SettingsStore
try:
store = SettingsStore(client)
data = await store.load()
stored = data.get("pending_approvals", [])
stored_blocked = data.get("blocked_ships", [])
if isinstance(stored_blocked, list):
for ship in stored_blocked:
self._blocked_ships.add(ship.lstrip("~").lower())
loaded: list[PendingApproval] = []
for item in stored:
approval = PendingApproval(
id=item.get("id", str(uuid.uuid4())[:8]),
approval_type=item.get("type", "dm"),
requester_ship=item.get("requester", ""),
channel_id=item.get("channel", ""),
message_content=item.get("content", ""),
created_at=item.get("created_at", 0),
status=item.get("status", "pending"),
)
if approval.status == "pending":
self._pending[approval.id] = approval
else:
self._resolved[approval.id] = approval
loaded.append(approval)
if loaded or stored_blocked:
logger.info(
f"[Urbit] Loaded {len(loaded)} approvals "
f"and {len(self._blocked_ships)} blocked ships from Settings Store"
)
return loaded
except Exception as e:
logger.warning(f"[Urbit] Failed to load approvals: {e}")
return []