refactor(urbit): 整理导入顺序并修复多处代码细节
1. 统一调整多个文件的导入排序,将TYPE_CHECKING相关导入放在正确位置 2. 修复rate_limiter中当限制数<=0时直接返回false的逻辑 3. 为message_cache新增更新消息内容的方法 4. 重构extract_graph_content支持日记类型内容提取 5. 调整@提及匹配的正则表达式,避免误匹配 6. 完善invite_manager,添加客户端和凭据支持并实现自动接受群邀请逻辑 7. 调整adapter.py中的导入顺序和初始化逻辑 8. 修复monitor中的编辑事件处理,改为异步处理并实现消息更新缓存 9. 调整datetime导入顺序,统一使用UTC在前的格式
This commit is contained in:
parent
18d1ea2aac
commit
80e3f66974
@ -1,15 +1,15 @@
|
||||
from yuxi.channels.adapters.urbit.adapter import UrbitAdapter
|
||||
from yuxi.channels.adapters.urbit.accounts import AccountManager, MultiAccountConnectionManager
|
||||
from yuxi.channels.adapters.urbit.invites import InviteManager
|
||||
from yuxi.channels.adapters.urbit.adapter import UrbitAdapter
|
||||
from yuxi.channels.adapters.urbit.config_ui_hints import get_categories, get_config_ui_hints, get_hints_by_category
|
||||
from yuxi.channels.adapters.urbit.directory import DirectoryQuery
|
||||
from yuxi.channels.adapters.urbit.dual_plugin import (
|
||||
UrbitSetupPlugin,
|
||||
UrbitAccountPlugin,
|
||||
DualPluginRegistry,
|
||||
UrbitAccountPlugin,
|
||||
UrbitSetupPlugin,
|
||||
get_global_registry,
|
||||
)
|
||||
from yuxi.channels.adapters.urbit.poke_api import HttpPokeApiClient, with_http_poke_api, create_http_poke_api
|
||||
from yuxi.channels.adapters.urbit.config_ui_hints import get_config_ui_hints, get_categories, get_hints_by_category
|
||||
from yuxi.channels.adapters.urbit.invites import InviteManager
|
||||
from yuxi.channels.adapters.urbit.poke_api import HttpPokeApiClient, create_http_poke_api, with_http_poke_api
|
||||
|
||||
__all__ = [
|
||||
"UrbitAdapter",
|
||||
|
||||
@ -7,12 +7,12 @@ from typing import Any
|
||||
|
||||
from yuxi.channels.base import BaseChannelAdapter
|
||||
from yuxi.channels.capabilities import ChannelCapabilities
|
||||
from yuxi.channels.meta import ChannelMeta
|
||||
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
|
||||
from yuxi.channels.exceptions import (
|
||||
ChannelAuthenticationError,
|
||||
ChannelConnectionError,
|
||||
)
|
||||
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
|
||||
from yuxi.channels.meta import ChannelMeta
|
||||
from yuxi.channels.models import (
|
||||
ChannelMessage,
|
||||
ChannelResponse,
|
||||
@ -25,32 +25,32 @@ from yuxi.channels.registry import register_builtin_adapter
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
from .accounts import AccountManager
|
||||
from .approval import ApprovalSystem
|
||||
from .auth import authenticate_ship
|
||||
from .authorization import ChannelAuthorization
|
||||
from .client import UrbitClient
|
||||
from .doctor import create_legacy_private_network_doctor_contract, apply_doctor_migrations
|
||||
from .discovery import ChannelDiscovery
|
||||
from .doctor import apply_doctor_migrations, create_legacy_private_network_doctor_contract
|
||||
from .format import build_channel_path, build_dm_channel_id, build_poke_payload
|
||||
from .history import MessageCache
|
||||
from .invites import InviteManager
|
||||
from .monitor import SSEDeduplicator, UrbitSSEManager, _graph_update_to_channel_message
|
||||
from .probe import probe_ship
|
||||
from .rate_limiter import RateLimiter
|
||||
from .send import (
|
||||
_send_media_poke,
|
||||
send_del_reaction_poke,
|
||||
send_delete_poke,
|
||||
send_edit_poke,
|
||||
send_poke,
|
||||
send_reaction_poke,
|
||||
send_del_reaction_poke,
|
||||
send_edit_poke,
|
||||
send_delete_poke,
|
||||
send_unblock_ship_poke,
|
||||
_send_media_poke,
|
||||
)
|
||||
from .session import resolve_session_route, detect_unsafe_session
|
||||
from .approval import ApprovalSystem
|
||||
from .accounts import AccountManager
|
||||
from .invites import InviteManager
|
||||
from .threads import ThreadManager
|
||||
from .authorization import ChannelAuthorization
|
||||
from .session import detect_unsafe_session, resolve_session_route
|
||||
from .settings import SettingsStore
|
||||
from .discovery import ChannelDiscovery
|
||||
from .history import MessageCache
|
||||
from .summarizer import Summarizer
|
||||
from .threads import ThreadManager
|
||||
|
||||
|
||||
@register_builtin_adapter
|
||||
@ -192,6 +192,10 @@ class UrbitAdapter(BaseChannelAdapter):
|
||||
|
||||
await probe_ship(self._client)
|
||||
|
||||
self._invite_manager._client = self._client
|
||||
self._invite_manager._ship_name = self._client.ship_name
|
||||
self._invite_manager._ship_code = self._ship_code
|
||||
|
||||
if self._settings_store is None and self._client:
|
||||
self._settings_store = SettingsStore(self._client)
|
||||
await self._settings_store.load(self.config)
|
||||
@ -469,7 +473,7 @@ class UrbitAdapter(BaseChannelAdapter):
|
||||
if rate_limit_result := await self._check_rate_limit():
|
||||
return rate_limit_result
|
||||
|
||||
from .format import build_channel_path, build_poke_payload, build_media_content
|
||||
from .format import build_channel_path, build_media_content, build_poke_payload
|
||||
|
||||
metadata: dict[str, str] = {
|
||||
"chat_type": "group",
|
||||
|
||||
@ -198,7 +198,6 @@ class ApprovalSystem:
|
||||
return None
|
||||
|
||||
async def persist_to_settings_store(self, store: SettingsStore) -> None:
|
||||
from .settings import SettingsStore
|
||||
|
||||
store_data: dict[str, Any] = {
|
||||
"pending_approvals": [],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ from typing import Any
|
||||
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
_LEGACY_KEY_MAP: dict[str, str] = {
|
||||
"allow_private_network": "network.dangerously_allow_private_network",
|
||||
"allowPrivateNetwork": "network.dangerouslyAllowPrivateNetwork",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
@ -53,7 +53,7 @@ def parse_graph_update(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
additions = graph_update.get("additions", {})
|
||||
if additions:
|
||||
result["content"] = _extract_graph_content(additions)
|
||||
result["content"] = _extract_graph_content(additions, resource_type=result.get("resource_type", "chat"))
|
||||
|
||||
if "reaction" in graph_update:
|
||||
result["reaction"] = graph_update["reaction"]
|
||||
@ -61,11 +61,17 @@ def parse_graph_update(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
return result
|
||||
|
||||
|
||||
def _extract_graph_content(additions: dict[str, Any]) -> str:
|
||||
def _extract_graph_content(additions: dict[str, Any], resource_type: str = "chat") -> str:
|
||||
parts: list[str] = []
|
||||
for node_data in additions.values():
|
||||
post = node_data.get("post", {})
|
||||
contents = post.get("contents", [])
|
||||
|
||||
if resource_type == "diary":
|
||||
title = post.get("title", "")
|
||||
if title:
|
||||
parts.append(f"\U0001f4dd {title}\n")
|
||||
|
||||
for item in contents:
|
||||
if "text" in item:
|
||||
parts.append(item["text"])
|
||||
|
||||
@ -2,8 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
@ -62,6 +61,14 @@ class MessageCache:
|
||||
async with self._lock:
|
||||
return msg_id in self._sent_ids
|
||||
|
||||
async def update_message(self, msg_id: str, new_content: str) -> bool:
|
||||
async with self._lock:
|
||||
for channel_cache in self._cache.values():
|
||||
if msg_id in channel_cache:
|
||||
channel_cache[msg_id]["content"] = new_content
|
||||
return True
|
||||
return False
|
||||
|
||||
async def clear_channel(self, channel_id: str) -> None:
|
||||
async with self._lock:
|
||||
self._cache.pop(channel_id, None)
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import UrbitClient
|
||||
|
||||
|
||||
class InviteManager:
|
||||
def __init__(
|
||||
@ -12,12 +15,18 @@ class InviteManager:
|
||||
group_invite_allowlist: list[str] | None = None,
|
||||
auto_accept_dm: bool = False,
|
||||
dm_allowlist: list[str] | None = None,
|
||||
client: UrbitClient | None = None,
|
||||
ship_name: str = "",
|
||||
ship_code: str = "",
|
||||
):
|
||||
self.auto_accept_groups = auto_accept_groups
|
||||
self.group_invite_allowlist = [s.lstrip("~").lower() for s in (group_invite_allowlist or [])]
|
||||
self.auto_accept_dm = auto_accept_dm
|
||||
self.dm_allowlist = [s.lstrip("~").lower() for s in (dm_allowlist or [])]
|
||||
self._pending_invites: dict[str, dict[str, Any]] = {}
|
||||
self._client = client
|
||||
self._ship_name = ship_name
|
||||
self._ship_code = ship_code
|
||||
|
||||
def should_auto_accept_group_invite(self, inviter_ship: str) -> bool:
|
||||
if not self.auto_accept_groups:
|
||||
@ -64,7 +73,48 @@ class InviteManager:
|
||||
return {"action": "left", "group": group, "ship": ship}
|
||||
|
||||
async def _accept_invite(self, group: str, inviter: str) -> None:
|
||||
logger.info(f"[Urbit] Would accept group invite: {group} from ~{inviter}")
|
||||
if not self._client:
|
||||
logger.warning(f"[Urbit] Cannot accept group invite {group} from ~{inviter}: client not available")
|
||||
return
|
||||
|
||||
logger.info(f"[Urbit] Accepting group invite: {group} from ~{inviter}")
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"action": "poke",
|
||||
"ship": self._ship_name,
|
||||
"app": "groups",
|
||||
"mark": "groups-action",
|
||||
"json": {
|
||||
"join": {
|
||||
"group": group,
|
||||
"ship": f"~{inviter.lstrip('~')}",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
r = await self._client.put(
|
||||
"/~/channel/groups-0",
|
||||
json=payload,
|
||||
timeout=10.0,
|
||||
)
|
||||
|
||||
if r.status_code == 401 and self._ship_code:
|
||||
from .auth import refresh_session_if_needed
|
||||
|
||||
await refresh_session_if_needed(self._client, self._ship_code)
|
||||
payload["ship"] = self._client.ship_name
|
||||
r = await self._client.put(
|
||||
"/~/channel/groups-0",
|
||||
json=payload,
|
||||
timeout=10.0,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
logger.info(f"[Urbit] Successfully accepted group invite: {group}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Urbit] Failed to accept group invite {group} from ~{inviter}: {e}")
|
||||
|
||||
def get_pending_invites(self) -> list[dict[str, Any]]:
|
||||
return list(self._pending_invites.values())
|
||||
|
||||
@ -6,9 +6,9 @@ import random
|
||||
import re
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, UTC
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
|
||||
@ -22,18 +22,18 @@ from yuxi.channels.models import (
|
||||
)
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
from .format import (
|
||||
parse_graph_update,
|
||||
ensure_ship_tilde,
|
||||
extract_graph_mentions,
|
||||
extract_graph_attachments,
|
||||
)
|
||||
from .cites import parse_cite
|
||||
from .format import (
|
||||
ensure_ship_tilde,
|
||||
extract_graph_attachments,
|
||||
extract_graph_mentions,
|
||||
parse_graph_update,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import UrbitClient
|
||||
from .threads import ThreadManager
|
||||
from .history import MessageCache
|
||||
from .threads import ThreadManager
|
||||
|
||||
_MAX_RECONNECT_DELAY = 30.0
|
||||
_INITIAL_RECONNECT_DELAY = 1.0
|
||||
@ -41,7 +41,7 @@ _MAX_RECONNECT_COUNT = 10
|
||||
_MAX_CONSECUTIVE_ERRORS = 5
|
||||
_JITTER = 0.1
|
||||
_ACK_INTERVAL = 20
|
||||
_ALL_PATTERN = re.compile(r"\b@(all|everyone|channel|here)\b", re.IGNORECASE)
|
||||
_ALL_PATTERN = re.compile(r"(?:^|\s)@(all|everyone|channel|here)\b", re.IGNORECASE)
|
||||
|
||||
|
||||
class SSEDeduplicator:
|
||||
@ -228,13 +228,9 @@ class UrbitSSEManager:
|
||||
logger.warning(f"[Urbit] SSE '{label}' connection lost: {e}. Reconnecting in {delay:.1f}s...")
|
||||
except Exception as e:
|
||||
consecutive_errors += 1
|
||||
logger.error(
|
||||
f"[Urbit] SSE '{label}' unexpected error (#{consecutive_errors}): {e}"
|
||||
)
|
||||
logger.error(f"[Urbit] SSE '{label}' unexpected error (#{consecutive_errors}): {e}")
|
||||
if consecutive_errors >= _MAX_CONSECUTIVE_ERRORS:
|
||||
logger.critical(
|
||||
f"[Urbit] SSE '{label}' aborted after {consecutive_errors} consecutive errors"
|
||||
)
|
||||
logger.critical(f"[Urbit] SSE '{label}' aborted after {consecutive_errors} consecutive errors")
|
||||
break
|
||||
|
||||
reconnect_count += 1
|
||||
@ -264,7 +260,7 @@ class UrbitSSEManager:
|
||||
|
||||
edit = graph_update.get("edit")
|
||||
if edit and isinstance(edit, dict):
|
||||
self._handle_edit_event(edit, channel_id, label)
|
||||
await self._handle_edit_event(edit, channel_id, label)
|
||||
return
|
||||
|
||||
index = str(graph_update.get("index", ""))
|
||||
@ -336,12 +332,46 @@ class UrbitSSEManager:
|
||||
except Exception as e:
|
||||
logger.error(f"[Urbit] SSE event processing error: {e}")
|
||||
|
||||
def _handle_edit_event(self, edit: dict[str, Any], channel_id: str, label: str) -> None:
|
||||
async def _handle_edit_event(self, edit: dict[str, Any], channel_id: str, label: str) -> None:
|
||||
index = edit.get("index", "")
|
||||
content_blocks = edit.get("content", [])
|
||||
extracted = self._extract_edit_content(content_blocks)
|
||||
if extracted:
|
||||
logger.info(f"[Urbit] SSE edit event at index={index}: content updated ({len(extracted)} chars)")
|
||||
if not extracted:
|
||||
return
|
||||
|
||||
logger.info(f"[Urbit] SSE edit event at index={index}: content updated ({len(extracted)} chars)")
|
||||
|
||||
resource = edit.get("resource", {})
|
||||
resource_path = resource.get("path", "")
|
||||
resource_type = resource.get("type", "chat")
|
||||
author = edit.get("author", "")
|
||||
|
||||
chat_type = ChatType.DIRECT if resource_type == "dm" else ChatType.GROUP
|
||||
identity = ChannelIdentity(
|
||||
channel_id="urbit",
|
||||
channel_type=ChannelType.URBIT,
|
||||
channel_user_id=ensure_ship_tilde(author),
|
||||
channel_chat_id=resource_path or "unknown",
|
||||
channel_message_id=str(index) if index else None,
|
||||
)
|
||||
|
||||
edit_msg = ChannelMessage(
|
||||
identity=identity,
|
||||
chat_type=chat_type,
|
||||
content=extracted,
|
||||
message_type="edit",
|
||||
metadata={
|
||||
"urbit_resource_type": resource_type,
|
||||
"urbit_ship": author,
|
||||
"chat_type": "direct" if resource_type == "dm" else "group",
|
||||
"edit_index": index,
|
||||
},
|
||||
)
|
||||
|
||||
asyncio.create_task(self._on_message(edit_msg))
|
||||
|
||||
if self._message_cache and index:
|
||||
await self._message_cache.update_message(str(index), extracted)
|
||||
|
||||
@staticmethod
|
||||
def _extract_edit_content(contents: list[dict[str, Any]]) -> str:
|
||||
@ -640,9 +670,9 @@ def _detect_bot_mentions(msg: ChannelMessage, bot_ship: str) -> MentionsInfo | N
|
||||
mentioned = False
|
||||
bot_with_tilde = f"~{bot_ship}"
|
||||
|
||||
if re.search(rf"\b@{bot_ship}\b", content, re.IGNORECASE):
|
||||
if re.search(rf"(?:^|\s)@{bot_ship}\b", content, re.IGNORECASE):
|
||||
mentioned = True
|
||||
if re.search(rf"\b{bot_with_tilde}\b", content, re.IGNORECASE):
|
||||
if re.search(rf"(?:^|\s){bot_with_tilde}\b", content, re.IGNORECASE):
|
||||
mentioned = True
|
||||
if not mentioned and "additions" in str(msg.metadata):
|
||||
additions = msg.metadata.get("raw_additions", {})
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
@ -15,6 +15,8 @@ class RateLimiter:
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def acquire(self) -> bool:
|
||||
if self._limit <= 0:
|
||||
return False
|
||||
async with self._lock:
|
||||
now = time.monotonic()
|
||||
elapsed = now - self._last_refill
|
||||
@ -31,6 +33,8 @@ class RateLimiter:
|
||||
return False
|
||||
|
||||
async def wait_and_acquire(self, timeout: float = 30.0) -> bool:
|
||||
if self._limit <= 0:
|
||||
return False
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if await self.acquire():
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import random
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
@ -66,10 +66,7 @@ def clear_unsafe_sessions() -> None:
|
||||
|
||||
def cleanup_expired_sessions() -> int:
|
||||
now = time.monotonic()
|
||||
expired = [
|
||||
key for key, ts in _session_last_access.items()
|
||||
if now - ts > _SESSION_TTL_S
|
||||
]
|
||||
expired = [key for key, ts in _session_last_access.items() if now - ts > _SESSION_TTL_S]
|
||||
for key in expired:
|
||||
_unsafe_sessions.pop(key, None)
|
||||
_session_last_access.pop(key, None)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import hashlib
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
|
||||
@ -129,7 +129,7 @@ async def _upload_via_s3(
|
||||
|
||||
try:
|
||||
import hmac
|
||||
from datetime import datetime, UTC
|
||||
from datetime import UTC, datetime
|
||||
|
||||
date_short = datetime.now(UTC).strftime("%Y%m%d")
|
||||
date_long = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user