这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
145 lines
5.0 KiB
Python
145 lines
5.0 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
|
|
from .settings import SettingsStore
|
|
|
|
|
|
class ThreadManager:
|
|
def __init__(self, max_history: int = 20, context_lines: int = 10):
|
|
self._participated: set[str] = set()
|
|
self._max_history = max_history
|
|
self._context_lines = context_lines
|
|
self._settings_store: SettingsStore | None = None
|
|
|
|
def bind_settings_store(self, store: SettingsStore) -> None:
|
|
self._settings_store = store
|
|
|
|
async def restore_from_store(self) -> None:
|
|
if not self._settings_store:
|
|
return
|
|
stored = self._settings_store.get("participatedThreads", [])
|
|
if isinstance(stored, list):
|
|
self._participated = set(stored)
|
|
|
|
async def persist_participated(self) -> None:
|
|
if not self._settings_store:
|
|
return
|
|
self._settings_store.update_key("participatedThreads", list(self._participated))
|
|
|
|
def is_thread_reply(self, raw_data: dict[str, Any]) -> bool:
|
|
graph_update = raw_data.get("graph-update", {})
|
|
additions = graph_update.get("additions", {})
|
|
for node_data in additions.values():
|
|
post = node_data.get("post", {})
|
|
contents = post.get("contents", [])
|
|
for item in contents:
|
|
if isinstance(item, dict) and "reply" in item:
|
|
return True
|
|
return False
|
|
|
|
def get_reply_parent_id(self, raw_data: dict[str, Any]) -> str | None:
|
|
graph_update = raw_data.get("graph-update", {})
|
|
additions = graph_update.get("additions", {})
|
|
for node_data in additions.values():
|
|
post = node_data.get("post", {})
|
|
contents = post.get("contents", [])
|
|
for item in contents:
|
|
if isinstance(item, dict) and "reply" in item:
|
|
reply_data = item["reply"]
|
|
return reply_data.get("id") or reply_data
|
|
return None
|
|
|
|
def add_participated(self, thread_id: str) -> None:
|
|
self._participated.add(thread_id)
|
|
|
|
def has_participated(self, thread_id: str) -> bool:
|
|
return thread_id in self._participated
|
|
|
|
async def fetch_thread_history(
|
|
self, client: UrbitClient, ship_name: str, resource_path: str, parent_id: str
|
|
) -> list[dict[str, Any]]:
|
|
from .scry import scry_get
|
|
|
|
try:
|
|
path = f"/graph/{ship_name}/{resource_path}"
|
|
result = await scry_get(client, "graph", path)
|
|
if not result or not isinstance(result, dict):
|
|
return []
|
|
|
|
nodes = result.get("graph", {}).get("nodes", {})
|
|
thread_messages: list[dict[str, Any]] = []
|
|
|
|
for _node_id, node in nodes.items():
|
|
post = node.get("post", {})
|
|
if not post:
|
|
continue
|
|
if _is_child_of(parent_id, node):
|
|
thread_messages.append(_extract_message_content(node))
|
|
|
|
thread_messages.sort(key=lambda m: m.get("time", 0))
|
|
return thread_messages[-self._max_history :]
|
|
|
|
except Exception as e:
|
|
logger.warning(f"[Urbit] Failed to fetch thread history: {e}")
|
|
return []
|
|
|
|
def build_thread_context(self, history: list[dict[str, Any]]) -> str:
|
|
recent = history[-self._context_lines :]
|
|
if not recent:
|
|
return ""
|
|
lines: list[str] = [
|
|
f"[Thread conversation - {len(recent)} previous replies...]",
|
|
"[Previous messages]",
|
|
]
|
|
for msg in recent:
|
|
author = msg.get("author", "unknown")
|
|
content = msg.get("content", "")
|
|
lines.append(f"~{author}: {content}")
|
|
lines.append("[Current message]")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _is_child_of(parent_id: str, node: dict[str, Any]) -> bool:
|
|
children = node.get("children", {})
|
|
if parent_id in children:
|
|
return True
|
|
for child_node in children.values():
|
|
if isinstance(child_node, dict) and _is_child_of(parent_id, child_node):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _extract_message_content(node: dict[str, Any]) -> dict[str, Any]:
|
|
post = node.get("post", {})
|
|
author = post.get("author", "")
|
|
index = post.get("index", "")
|
|
time_sent = node.get("post", {}).get("time-sent", 0)
|
|
|
|
contents = post.get("contents", [])
|
|
text_parts: list[str] = []
|
|
for item in contents:
|
|
if "text" in item:
|
|
text_parts.append(item["text"])
|
|
elif "mention" in item:
|
|
text_parts.append(f"~{item['mention']}")
|
|
elif "url" in item:
|
|
text_parts.append(item["url"])
|
|
elif "code" in item:
|
|
code_block = item["code"]
|
|
if isinstance(code_block, dict):
|
|
text_parts.append(code_block.get("code", ""))
|
|
else:
|
|
text_parts.append(str(code_block))
|
|
|
|
return {
|
|
"author": author,
|
|
"index": index,
|
|
"content": "".join(text_parts),
|
|
"time": time_sent,
|
|
}
|