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

190 lines
5.9 KiB
Python

from __future__ import annotations
from typing import Any, TYPE_CHECKING
from yuxi.utils.logging_config import logger
if TYPE_CHECKING:
from .client import UrbitClient
_MIGRATABLE_KEYS = [
"dmAllowlist",
"groupChannels",
"autoDiscoverChannels",
"ownerShip",
"autoAcceptGroupInvites",
"autoAcceptDmInvites",
"groupInviteAllowlist",
]
class SettingsStore:
def __init__(self, client: UrbitClient, desk: str = "moltbot"):
self._client = client
self._desk = desk
self._store: dict[str, Any] = {}
self._file_config: dict[str, Any] = {}
self._loaded = False
self._version: int = 0
self._key_versions: dict[str, int] = {}
async def load(self, file_config: dict[str, Any] | None = None) -> dict[str, Any]:
if file_config:
self._file_config = file_config
try:
from .scry import scry_settings
store_data = await scry_settings(self._client, self._desk)
if store_data and isinstance(store_data, dict):
self._store = store_data
self._version += 1
logger.info(f"[Urbit] Settings Store loaded from desk '{self._desk}' (v{self._version})")
else:
logger.debug(f"[Urbit] Settings Store empty or not found for desk '{self._desk}'")
except Exception as e:
logger.warning(f"[Urbit] Failed to load Settings Store: {e}")
self._loaded = True
return self._store
def get(self, key: str, default: Any = None) -> Any:
store_val = self._store.get(key)
if store_val is not None:
if isinstance(store_val, list) and len(store_val) == 0:
return []
return store_val
return self._file_config.get(key, default)
def get_all(self) -> dict[str, Any]:
merged = dict(self._file_config)
for key, value in self._store.items():
if value is not None:
merged[key] = value
return merged
def merge_key(self, key: str, value: Any, expected_version: int | None = None) -> bool:
if expected_version is not None:
current_version = self._key_versions.get(key, 0)
if expected_version < current_version:
logger.debug(
f"[Urbit] Settings merge rejected for '{key}': version {expected_version} < {current_version}"
)
return False
store = self._store.copy()
store[key] = value
self._store = store
self._key_versions[key] = self._key_versions.get(key, 0) + 1
return True
def replace_all(self, data: dict[str, Any]) -> None:
self._store = dict(data)
self._version += 1
logger.debug(f"[Urbit] Settings Store replaced (v{self._version})")
def update_key(self, key: str, value: Any) -> None:
store = dict(self._store)
store[key] = value
self._store = store
self._key_versions[key] = self._key_versions.get(key, 0) + 1
self._version += 1
async def reload(self) -> None:
self._version += 1
await self.load(self._file_config)
async def build_settings_migrations(self) -> list[dict[str, Any]]:
migrations: list[dict[str, Any]] = []
for key in _MIGRATABLE_KEYS:
if self.should_migrate_setting(key):
migrations.append(
{
"bucket_key": "moltbot",
"entry_key": key,
"value": self._file_config.get(key),
}
)
return migrations
def should_migrate_setting(self, key: str) -> bool:
file_val = self._file_config.get(key)
if file_val is None:
return False
store_val = self._store.get(key)
return store_val is None
async def poke_migration(self, bucket_key: str, entry_key: str, value: Any) -> bool:
import json as _json
try:
value_str = _json.dumps(value) if not isinstance(value, str) else value
payload = {
"action": "poke",
"ship": self._client.ship_name,
"app": "settings",
"mark": "settings-event",
"json": {
"put-entry": {
"bucket-key": bucket_key,
"entry-key": entry_key,
"value": value_str,
}
},
}
r = await self._client.put("/~/channel/settings-store", json=payload, timeout=10.0)
r.raise_for_status()
logger.info(f"[Urbit] Migrated setting: {entry_key} → Settings Store")
return True
except Exception as e:
logger.warning(f"[Urbit] Migration failed for {entry_key}: {e}")
return False
@property
def is_loaded(self) -> bool:
return self._loaded
@property
def version(self) -> int:
return self._version
def parse_channel_rules(raw: Any) -> dict[str, Any] | None:
if raw is None:
return None
if isinstance(raw, dict):
return raw
if isinstance(raw, str):
import json as _json
try:
return _json.loads(raw)
except _json.JSONDecodeError:
logger.warning("[Urbit] parse_channel_rules: invalid JSON string")
return None
return None
def parse_pending_approvals(raw: Any) -> list[dict[str, Any]] | None:
if raw is None:
return None
if isinstance(raw, list):
return raw
if isinstance(raw, str):
import json as _json
try:
return _json.loads(raw)
except _json.JSONDecodeError:
logger.warning("[Urbit] parse_pending_approvals: invalid JSON string")
return None
return None
def serialize_to_json_value(value: Any) -> str:
import json as _json
return _json.dumps(value)