-
-
Notifications
You must be signed in to change notification settings - Fork 932
feat:Misskey 适配器支持文件上传、投票内容感知功能和重构部分代码 #2986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat:Misskey 适配器支持文件上传、投票内容感知功能和重构部分代码 #2986
Conversation
- 重构了 `misskey_event.py` 中的 `send` 方法,使用新的适配器方法 `send_by_session`,以改进消息处理(包括文件上传)。 - 添加了详细的日志记录,以提高消息发送过程的可追溯性。 - 在 `misskey_utils.py` 中引入了 `FileIDExtractor` 和 `MessagePayloadBuilder` 类,以简化文件 ID 提取和消息载荷构建。 - 在 `misskey_utils.py` 中实现了 MIME 类型检测和文件扩展名解析,以支持多种文件上传。 - 增强了 `resolve_component_url_or_path`,以更好地处理不同类型的组件上传文件。 - 在 `upload_local_with_retries` 中添加了重试逻辑,以优雅地处理不允许的文件类型。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:480-489` </location>
<code_context>
+from .misskey_api import MisskeyAPI, APIError
+import os
+
+try:
+ import magic # type: ignore
+except Exception:
</code_context>
<issue_to_address>
**suggestion (performance):** File upload uses blocking file I/O in async context.
Using synchronous file I/O in async code can block the event loop. Switch to aiofiles or another async file library for non-blocking access.
Suggested implementation:
```python
import os
import aiofiles
try:
import magic # type: ignore
except Exception:
pass
```
```python
# Asynchronous file I/O using aiofiles
async with aiofiles.open(file_path, "rb") as f:
file_bytes = await f.read()
```
If there are other places in the file where synchronous file I/O is used for uploads or downloads, you should similarly replace them with `aiofiles` and use `async with` and `await`. Make sure the containing function is declared as `async def` if it isn't already.
</issue_to_address>
### Comment 2
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:782-789` </location>
<code_context>
+ """使用 session 流式读取并计算 MD5,支持下载大小限制"""
+ total = 0
+ m = hashlib.md5()
+ async with self.session.get(
+ url,
+ timeout=aiohttp.ClientTimeout(total=self.download_timeout),
+ ssl=ssl_verify,
+ ) as resp:
+ if resp.status != 200:
+ return None
+ async for chunk in resp.content.iter_chunked(self.chunk_size):
+ if not chunk:
+ continue
</code_context>
<issue_to_address>
**suggestion:** No explicit handling for very large files in _stream_md5_with_session.
Consider implementing a hard limit or aborting the stream for excessively large files, particularly when max_download_bytes is None, to prevent potential memory issues.
Suggested implementation:
```python
MAX_STREAM_MD5_BYTES = 100 * 1024 * 1024 # 100MB, 可根据实际需求调整
async def _stream_md5_with_session(
self, url: str, ssl_verify: bool = True
) -> Optional[str]:
"""使用 session 流式读取并计算 MD5,支持下载大小限制和硬性最大下载字节数"""
total = 0
m = hashlib.md5()
```
```python
total = 0
m = hashlib.md5()
```
```python
async with self.session.get(
url,
timeout=aiohttp.ClientTimeout(total=self.download_timeout),
ssl=ssl_verify,
) as resp:
if resp.status != 200:
return None
async for chunk in resp.content.iter_chunked(self.chunk_size):
if not chunk:
continue
total += len(chunk)
if total > MAX_STREAM_MD5_BYTES:
logger.warning(f"[Misskey API] 文件过大,已超过最大流式 MD5 限制: {url}")
return None
m.update(chunk)
```
</issue_to_address>
### Comment 3
<location> `astrbot/core/platform/sources/misskey/misskey_adapter.py:449-454` </location>
<code_context>
+ session_id, text, session, message_chain
+ )
+
+ upload_concurrency = int(
+ self.config.get(
+ "misskey_upload_concurrency", DEFAULT_UPLOAD_CONCURRENCY
+ )
+ )
+ sem = asyncio.Semaphore(upload_concurrency)
+
+ async def _upload_comp(comp) -> Optional[object]:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** No upper bound on upload_concurrency from config.
Enforce a maximum value for upload_concurrency (e.g., 10) to avoid resource exhaustion from excessive parallel uploads.
```suggestion
MAX_UPLOAD_CONCURRENCY = 10
upload_concurrency = int(
self.config.get(
"misskey_upload_concurrency", DEFAULT_UPLOAD_CONCURRENCY
)
)
upload_concurrency = min(upload_concurrency, MAX_UPLOAD_CONCURRENCY)
sem = asyncio.Semaphore(upload_concurrency)
```
</issue_to_address>
### Comment 4
<location> `astrbot/core/platform/sources/misskey/misskey_adapter.py:851` </location>
<code_context>
file_ids=file_ids if file_ids else None,
</code_context>
<issue_to_address>
**suggestion (code-quality):** Replace if-expression with `or` ([`or-if-exp-identity`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/or-if-exp-identity))
```suggestion
file_ids=file_ids or None,
```
<br/><details><summary>Explanation</summary>Here we find ourselves setting a value if it evaluates to `True`, and otherwise
using a default.
The 'After' case is a bit easier to read and avoids the duplication of
`input_currency`.
It works because the left-hand side is evaluated first. If it evaluates to
true then `currency` will be set to this and the right-hand side will not be
evaluated. If it evaluates to false the right-hand side will be evaluated and
`currency` will be set to `DEFAULT_CURRENCY`.
</details>
</issue_to_address>
### Comment 5
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:222` </location>
<code_context>
f"text={'[no-text]' if not text else text[:80]} | files={has_files} | hidden={is_hidden}"
</code_context>
<issue_to_address>
**suggestion (code-quality):** Swap if/else branches of if expression to remove negation ([`swap-if-expression`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/swap-if-expression))
```suggestion
f"text={text[:80] if text else '[no-text]'} | files={has_files} | hidden={is_hidden}"
```
<br/><details><summary>Explanation</summary>Negated conditions are more difficult to read than positive ones, so it is best
to avoid them where we can. By swapping the `if` and `else` conditions around we
can invert the condition and make it positive.
</details>
</issue_to_address>
### Comment 6
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:803` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Raise a specific error instead of the general `Exception` or `BaseException` ([`raise-specific-error`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/raise-specific-error))
<details><summary>Explanation</summary>If a piece of code raises a specific exception type
rather than the generic
[`BaseException`](https://docs.python.org/3/library/exceptions.html#BaseException)
or [`Exception`](https://docs.python.org/3/library/exceptions.html#Exception),
the calling code can:
- get more information about what type of error it is
- define specific exception handling for it
This way, callers of the code can handle the error appropriately.
How can you solve this?
- Use one of the [built-in exceptions](https://docs.python.org/3/library/exceptions.html) of the standard library.
- [Define your own error class](https://docs.python.org/3/tutorial/errors.html#tut-userexceptions) that subclasses `Exception`.
So instead of having code raising `Exception` or `BaseException` like
```python
if incorrect_input(value):
raise Exception("The input is incorrect")
```
you can have code raising a specific error like
```python
if incorrect_input(value):
raise ValueError("The input is incorrect")
```
or
```python
class IncorrectInputError(Exception):
pass
if incorrect_input(value):
raise IncorrectInputError("The input is incorrect")
```
</details>
</issue_to_address>
### Comment 7
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:878-880` </location>
<code_context>
tmp_bytes = await self._download_with_existing_session(url)
if not tmp_bytes:
tmp_bytes = await self._download_with_temp_session(url)
</code_context>
<issue_to_address>
**suggestion (code-quality):** Use `or` for providing a fallback value ([`use-or-for-fallback`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/use-or-for-fallback))
```suggestion
tmp_bytes = await self._download_with_existing_session(url) or await self._download_with_temp_session(url)
```
<br/><details><summary>Explanation</summary>Thanks to the flexibility of Python's `or` operator, you can use a single
assignment statement, even if a variable can retrieve its value from various
sources. This is shorter and easier to read than using multiple assignments with
`if not` conditions.
</details>
</issue_to_address>
### Comment 8
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:1205` </location>
<code_context>
"file_ids": file_ids if file_ids else None,
</code_context>
<issue_to_address>
**suggestion (code-quality):** Replace if-expression with `or` ([`or-if-exp-identity`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/or-if-exp-identity))
```suggestion
"file_ids": file_ids or None,
```
<br/><details><summary>Explanation</summary>Here we find ourselves setting a value if it evaluates to `True`, and otherwise
using a default.
The 'After' case is a bit easier to read and avoids the duplication of
`input_currency`.
It works because the left-hand side is evaluated first. If it evaluates to
true then `currency` will be set to this and the right-hand side will not be
evaluated. If it evaluates to false the right-hand side will be evaluated and
`currency` will be set to `DEFAULT_CURRENCY`.
</details>
</issue_to_address>
### Comment 9
<location> `astrbot/core/platform/sources/misskey/misskey_adapter.py:201-202` </location>
<code_context>
def _process_poll_data(
self, message: AstrBotMessage, poll: Dict[str, Any], message_parts: List[str]
):
"""处理投票数据,将其添加到消息中"""
try:
if not isinstance(message.raw_message, dict):
message.raw_message = {}
message.raw_message["poll"] = poll
setattr(message, "poll", poll)
except Exception:
pass
poll_text = format_poll(poll)
if poll_text:
message.message.append(Comp.Plain(poll_text))
message_parts.append(poll_text)
</code_context>
<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
```suggestion
if poll_text := format_poll(poll):
```
</issue_to_address>
### Comment 10
<location> `astrbot/core/platform/sources/misskey/misskey_adapter.py:219-225` </location>
<code_context>
def _extract_additional_fields(self, session, message_chain) -> Dict[str, Any]:
"""从会话和消息链中提取额外字段"""
fields = {"cw": None, "poll": None, "renote_id": None, "channel_id": None}
for comp in message_chain.chain:
if hasattr(comp, "cw") and getattr(comp, "cw", None):
fields["cw"] = getattr(comp, "cw")
break
if hasattr(session, "extra_data") and isinstance(
getattr(session, "extra_data", None), dict
):
extra_data = getattr(session, "extra_data")
fields.update(
{
"poll": extra_data.get("poll"),
"renote_id": extra_data.get("renote_id"),
"channel_id": extra_data.get("channel_id"),
}
)
return fields
</code_context>
<issue_to_address>
**suggestion (code-quality):** Merge dictionary updates via the union operator ([`dict-assign-update-to-union`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/dict-assign-update-to-union/))
```suggestion
fields |= {
"poll": extra_data.get("poll"),
"renote_id": extra_data.get("renote_id"),
"channel_id": extra_data.get("channel_id"),
}
```
</issue_to_address>
### Comment 11
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:194` </location>
<code_context>
async def _handle_message(self, data: Dict[str, Any]):
message_type = data.get("type")
body = data.get("body", {})
try:
body = data.get("body") or {}
channel_summary = None
if isinstance(body, dict):
inner = body.get("body") if isinstance(body.get("body"), dict) else body
note = None
if isinstance(inner, dict) and isinstance(inner.get("note"), dict):
note = inner.get("note")
text = None
has_files = False
is_hidden = False
note_id = None
user = None
if note:
text = note.get("text")
note_id = note.get("id")
files = note.get("files") or []
has_files = bool(files)
is_hidden = bool(note.get("isHidden"))
user = note.get("user", {})
channel_summary = (
f"[Misskey WebSocket] 收到消息类型: {message_type} | "
f"note_id={note_id} | user={user.get('username') if user else None} | "
f"text={'[no-text]' if not text else text[:80]} | files={has_files} | hidden={is_hidden}"
)
else:
channel_summary = f"[Misskey WebSocket] 收到消息类型: {message_type}"
logger.info(channel_summary)
except Exception:
logger.info(f"[Misskey WebSocket] 收到消息类型: {message_type}")
logger.debug(
f"[Misskey WebSocket] 收到完整消息: {json.dumps(data, indent=2, ensure_ascii=False)}"
)
if message_type == "channel":
channel_id = body.get("id")
event_type = body.get("type")
event_body = body.get("body", {})
logger.debug(
f"[Misskey WebSocket] 频道消息: {channel_id}, 事件类型: {event_type}"
)
if channel_id in self.channels:
channel_type = self.channels[channel_id]
handler_key = f"{channel_type}:{event_type}"
if handler_key in self.message_handlers:
logger.debug(f"[Misskey WebSocket] 使用处理器: {handler_key}")
await self.message_handlers[handler_key](event_body)
elif event_type in self.message_handlers:
logger.debug(f"[Misskey WebSocket] 使用事件处理器: {event_type}")
await self.message_handlers[event_type](event_body)
else:
logger.debug(
f"[Misskey WebSocket] 未找到处理器: {handler_key} 或 {event_type}"
)
if "_debug" in self.message_handlers:
await self.message_handlers["_debug"](
{
"type": event_type,
"body": event_body,
"channel": channel_type,
}
)
elif message_type in self.message_handlers:
logger.debug(f"[Misskey WebSocket] 直接消息处理器: {message_type}")
await self.message_handlers[message_type](body)
else:
logger.debug(f"[Misskey WebSocket] 未处理的消息类型: {message_type}")
if "_debug" in self.message_handlers:
await self.message_handlers["_debug"](data)
</code_context>
<issue_to_address>
**issue (code-quality):** Low code quality found in StreamingClient.\_handle\_message - 13% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))
<br/><details><summary>Explanation</summary>The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.
How can you solve this?
It might be worth refactoring this function to make it shorter and more readable.
- Reduce the function length by extracting pieces of functionality out into
their own functions. This is the most important thing you can do - ideally a
function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
sits together within the function rather than being scattered.</details>
</issue_to_address>
### Comment 12
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:354-356` </location>
<code_context>
def __init__(
self,
instance_url: str,
access_token: str,
*,
allow_insecure_downloads: bool = False,
download_timeout: int = 15,
chunk_size: int = 64 * 1024,
max_download_bytes: Optional[int] = None,
):
self.instance_url = instance_url.rstrip("/")
self.access_token = access_token
self._session: Optional[aiohttp.ClientSession] = None
self.streaming: Optional[StreamingClient] = None
# download options
self.allow_insecure_downloads = bool(allow_insecure_downloads)
self.download_timeout = int(download_timeout)
self.chunk_size = int(chunk_size)
self.max_download_bytes = (
int(max_download_bytes) if max_download_bytes is not None else None
)
</code_context>
<issue_to_address>
**suggestion (code-quality):** Remove unnecessary casts to int, str, float or bool [×3] ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
```suggestion
self.allow_insecure_downloads = allow_insecure_downloads
self.download_timeout = download_timeout
self.chunk_size = chunk_size
```
</issue_to_address>
### Comment 13
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:725` </location>
<code_context>
async def calculate_url_md5(self, url: str) -> Optional[str]:
"""Calculate MD5 hash of file from URL with fallback strategies"""
if not url:
return None
# 1) 尝试 HEAD 查找 Content-MD5 / ETag(轻量)
try:
async with self.session.head(
url, timeout=aiohttp.ClientTimeout(total=self.download_timeout)
) as head_resp:
if head_resp.status == 200:
# Content-MD5 header 通常是 base64(md5)
content_md5 = head_resp.headers.get("Content-MD5")
if content_md5:
try:
raw = base64.b64decode(content_md5)
hex_md5 = raw.hex()
logger.debug(
f"[Misskey API] 从 HEAD Content-MD5 获取 md5: {hex_md5}"
)
return hex_md5
except Exception:
logger.debug(
"[Misskey API] 无法解析 Content-MD5 header,继续流式下载"
)
# 尝试 ETag(有些服务直接返回十六进制 MD5)
etag = head_resp.headers.get("ETag")
if etag:
etag_val = etag.strip('"')
if re.fullmatch(r"[0-9a-fA-F]{32}", etag_val):
logger.debug(
f"[Misskey API] 从 HEAD ETag 获取 md5: {etag_val}"
)
return etag_val.lower()
except Exception:
logger.debug("[Misskey API] HEAD 请求失败,继续使用流式 GET")
# 2) 流式 GET(使用现有 session)
try:
md5 = await self._stream_md5_with_session(url, ssl_verify=True)
if md5:
logger.debug(f"[Misskey API] 流式 MD5 计算成功 (ssl): {md5}")
return md5
except Exception as e:
logger.debug(f"[Misskey API] 流式下载(ssl)失败: {e}")
# 3) 可选:不安全下载(受配置控制,仅做最后退路)
if self.allow_insecure_downloads:
try:
md5 = await self._stream_md5_with_session(url, ssl_verify=False)
if md5:
logger.warning(
f"[Misskey API] 使用不安全下载获取 MD5(ssl 验证已禁用): {url}"
)
return md5
except Exception as e:
logger.debug(f"[Misskey API] 不安全流式下载失败: {e}")
logger.warning(f"[Misskey API] 无法计算 MD5: {url}")
return None
</code_context>
<issue_to_address>
**issue (code-quality):** Use named expression to simplify assignment and conditional [×2] ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
</issue_to_address>
### Comment 14
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:851` </location>
<code_context>
async def upload_and_find_file(
self,
url: str,
name: Optional[str] = None,
folder_id: Optional[str] = None,
max_wait_time: float = 30.0,
check_interval: float = 2.0,
) -> Optional[Dict[str, Any]]:
"""
智能文件上传:先检查重复,再上传,最后轮询查找
Args:
url: 文件URL
name: 文件名(可选)
folder_id: 文件夹ID(可选)
max_wait_time: 最大等待时间
check_interval: 轮询间隔
Returns:
包含文件ID和元信息的字典,失败时返回None
"""
if not url:
raise APIError("URL不能为空")
# 优先按文件名在已有文件中查找(避免重复上传)
try:
filename = name or url.split("/")[-1].split("?")[0]
if filename:
matches = await self.find_files_by_name(filename, folder_id)
if matches:
file_id = matches[0].get("id")
logger.debug(f"[Misskey API] 通过名称找到已存在文件: {file_id}")
return {"id": file_id, "raw": matches[0], "name_match": True}
except Exception:
# 名称查找失败时继续尝试 upload-from-url
logger.debug("[Misskey API] 名称查找失败或异常,继续处理")
# 尝试使用 Misskey 的 upload-from-url 接口(服务器端处理远程 URL)
try:
upload_result = await self.upload_file_from_url(url, name, folder_id)
# 处理 upload-from-url 的返回(可能为 accepted 或直接返回文件)
md5_hash = await self.calculate_url_md5(url)
return await self._handle_upload_result(
upload_result, md5_hash, url, max_wait_time, check_interval
)
except Exception as e:
logger.warning(
f"[Misskey API] upload-from-url 失败,准备回退到下载并本地上传: {e}"
)
# 回退:下载远端文件并做本地上传
try:
# 使用现有 session 下载内容到临时文件
tmp_bytes = await self._download_with_existing_session(url)
if not tmp_bytes:
tmp_bytes = await self._download_with_temp_session(url)
if tmp_bytes:
# 写入临时文件并上传本地文件
import tempfile
with tempfile.NamedTemporaryFile(delete=False) as tmpf:
tmpf.write(tmp_bytes)
tmp_path = tmpf.name
try:
result = await self.upload_file(tmp_path, name, folder_id)
return result
finally:
import os
try:
os.unlink(tmp_path)
except Exception:
pass
except Exception as e:
logger.error(f"[Misskey API] 下载并本地上传回退失败: {e}")
return None
</code_context>
<issue_to_address>
**issue (code-quality):** We've found these issues:
- Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
- Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/inline-immediately-returned-variable/))
</issue_to_address>
### Comment 15
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:960-961` </location>
<code_context>
async def _poll_by_hash(
self, md5_hash: str, max_wait_time: float, check_interval: float, url: str
) -> Optional[Dict[str, Any]]:
"""通过MD5哈希轮询查找文件"""
logger.debug(f"[Misskey API] 开始轮询查找文件: {md5_hash}")
waited_time = 0.0
while waited_time < max_wait_time:
try:
files = await self.find_files_by_hash(md5_hash)
if files:
file_id = files[0].get("id")
if file_id:
logger.debug(f"[Misskey API] 异步上传完成: {file_id}")
return {"id": file_id, "raw": files[0], "async_found": True}
except Exception as e:
logger.debug(f"[Misskey API] 轮询查找出错: {e}")
await asyncio.sleep(check_interval)
waited_time += check_interval
# MD5轮询超时,尝试名称匹配
return await self._fallback_name_search(url)
</code_context>
<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
```suggestion
if file_id := files[0].get("id"):
```
</issue_to_address>
### Comment 16
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:1055-1057` </location>
<code_context>
async def get_messages(
self, user_id: str, limit: int = 10, since_id: Optional[str] = None
) -> List[Dict[str, Any]]:
"""获取聊天消息历史"""
data: Dict[str, Any] = {"userId": user_id, "limit": limit}
if since_id:
data["sinceId"] = since_id
result = await self._make_request("chat/messages/user-timeline", data)
if isinstance(result, list):
return result
else:
logger.warning(f"[Misskey API] 聊天消息响应格式异常: {type(result)}")
return []
</code_context>
<issue_to_address>
**suggestion (code-quality):** Remove unnecessary else after guard condition ([`remove-unnecessary-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-else/))
```suggestion
logger.warning(f"[Misskey API] 聊天消息响应格式异常: {type(result)}")
return []
```
</issue_to_address>
### Comment 17
<location> `astrbot/core/platform/sources/misskey/misskey_api.py:1163` </location>
<code_context>
async def _dispatch_message(
self,
message_type: str,
target_id: str,
text: Optional[str],
file_ids: List[str],
**kwargs,
) -> Dict[str, Any]:
"""根据消息类型分发到对应的发送方法"""
if message_type == "chat":
# 聊天消息使用 fileId (单数)
payload = {"toUserId": target_id}
if text:
payload["text"] = text
if file_ids:
if len(file_ids) == 1:
payload["fileId"] = file_ids[0]
else:
# 多文件时逐个发送
results = []
for file_id in file_ids:
single_payload = payload.copy()
single_payload["fileId"] = file_id
result = await self.send_message(single_payload)
results.append(result)
return {"multiple": True, "results": results}
return await self.send_message(payload)
elif message_type == "room":
# 房间消息使用 fileId (单数)
payload = {"toRoomId": target_id}
if text:
payload["text"] = text
if file_ids:
if len(file_ids) == 1:
payload["fileId"] = file_ids[0]
else:
# 多文件时逐个发送
results = []
for file_id in file_ids:
single_payload = payload.copy()
single_payload["fileId"] = file_id
result = await self.send_room_message(single_payload)
results.append(result)
return {"multiple": True, "results": results}
return await self.send_room_message(payload)
elif message_type == "note":
# 发帖使用 fileIds (复数)
note_kwargs = {
"text": text,
"file_ids": file_ids if file_ids else None,
}
# 合并其他参数
note_kwargs.update(kwargs)
return await self.create_note(**note_kwargs)
else:
raise APIError(f"不支持的消息类型: {message_type}")
</code_context>
<issue_to_address>
**issue (code-quality):** We've found these issues:
- Simplify conditional into switch-like form ([`switch`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/switch/))
- Merge dictionary updates via the union operator [×2] ([`dict-assign-update-to-union`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/dict-assign-update-to-union/))
</issue_to_address>
### Comment 18
<location> `astrbot/core/platform/sources/misskey/misskey_utils.py:26-27` </location>
<code_context>
@staticmethod
def extract_file_id(result: Any) -> Optional[str]:
if not isinstance(result, dict):
return None
id_paths = [
lambda r: r.get("createdFile", {}).get("id"),
lambda r: r.get("file", {}).get("id"),
lambda r: r.get("id"),
]
for p in id_paths:
try:
fid = p(result)
if fid:
return fid
except Exception:
continue
return None
</code_context>
<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
```suggestion
if fid := p(result):
```
</issue_to_address>
### Comment 19
<location> `astrbot/core/platform/sources/misskey/misskey_utils.py:69` </location>
<code_context>
@staticmethod
def build_note_payload(
text: Optional[str], file_ids: Optional[List[str]] = None, **kwargs
) -> Dict[str, Any]:
payload: Dict[str, Any] = {}
if text:
payload["text"] = text
if file_ids:
payload["fileIds"] = file_ids
payload.update(kwargs)
return payload
</code_context>
<issue_to_address>
**suggestion (code-quality):** Merge dictionary updates via the union operator ([`dict-assign-update-to-union`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/dict-assign-update-to-union/))
```suggestion
payload |= kwargs
```
</issue_to_address>
### Comment 20
<location> `astrbot/core/platform/sources/misskey/misskey_utils.py:283` </location>
<code_context>
def format_poll(poll: Dict[str, Any]) -> str:
"""将 Misskey 的 poll 对象格式化为可读字符串。"""
if not poll or not isinstance(poll, dict):
return ""
parts = []
multiple = poll.get("multiple", False)
choices = poll.get("choices", [])
parts.append("[投票]")
parts.append("允许多选" if multiple else "单选")
text_choices = []
for idx, c in enumerate(choices, start=1):
text = c.get("text", "")
votes = c.get("votes", 0)
text_choices.append(f"({idx}) {text} [{votes}票]")
if text_choices:
parts.append("选项: " + ", ".join(text_choices))
return " ".join(parts)
</code_context>
<issue_to_address>
**issue (code-quality):** We've found these issues:
- Merge consecutive list appends into a single extend ([`merge-list-appends-into-extend`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/merge-list-appends-into-extend/))
- Move assignment closer to its usage within a block ([`move-assign-in-block`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/move-assign-in-block/))
- Merge extend into list declaration ([`merge-list-extend`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/merge-list-extend/))
- Unwrap a constant iterable constructor ([`unwrap-iterable-construction`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/unwrap-iterable-construction/))
</issue_to_address>
### Comment 21
<location> `astrbot/core/platform/sources/misskey/misskey_utils.py:533-536` </location>
<code_context>
async def resolve_component_url_or_path(
comp: Any,
) -> Tuple[Optional[str], Optional[str]]:
"""尝试从组件解析可上传的远程 URL 或本地路径。
返回 (url_candidate, local_path)。两者可能都为 None。
这个函数尽量不抛异常,调用方可按需处理 None。
"""
url_candidate = None
local_path = None
try:
if hasattr(comp, "convert_to_file_path"):
try:
p = await comp.convert_to_file_path()
if isinstance(p, str):
if p.startswith("http"):
url_candidate = p
else:
local_path = p
except Exception:
pass
if not local_path and hasattr(comp, "get_file"):
try:
p = await comp.get_file()
if isinstance(p, str):
if p.startswith("http"):
url_candidate = p
else:
local_path = p
except Exception:
pass
# register_to_file_service or get_file(True) may provide a URL
if not url_candidate and hasattr(comp, "register_to_file_service"):
try:
r = await comp.register_to_file_service()
if isinstance(r, str) and r.startswith("http"):
url_candidate = r
except Exception:
pass
if not url_candidate and hasattr(comp, "get_file"):
try:
maybe = await comp.get_file(True)
if isinstance(maybe, str) and maybe.startswith("http"):
url_candidate = maybe
except Exception:
pass
# fallback to common attributes
if not url_candidate and not local_path:
for attr in ("file", "url", "path", "src", "source"):
try:
val = getattr(comp, attr, None)
except Exception:
val = None
if val and isinstance(val, str):
if val.startswith("http"):
url_candidate = val
break
else:
local_path = val
break
except Exception:
return None, None
return url_candidate, local_path
</code_context>
<issue_to_address>
**issue (code-quality):** Hoist repeated code outside conditional statement ([`hoist-statement-from-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/hoist-statement-from-if/))
</issue_to_address>
### Comment 22
<location> `astrbot/core/platform/sources/misskey/misskey_utils.py:566` </location>
<code_context>
async def upload_local_with_retries(
api: Any,
local_path: str,
preferred_name: Optional[str],
folder_id: Optional[str],
) -> Optional[str]:
"""尝试本地上传并在遇到 unallowed 错误时按扩展名重试,返回 file id 或 None。"""
try:
res = await api.upload_file(local_path, preferred_name, folder_id)
if isinstance(res, dict):
fid = res.get("id") or (res.get("raw") or {}).get("createdFile", {}).get(
"id"
)
if fid:
return str(fid)
except Exception as e:
msg = str(e).lower()
if "unallowed" in msg or "unallowed_file_type" in msg:
base = os.path.basename(local_path)
name_root, ext = os.path.splitext(base)
try_ext = detect_mime_ext(local_path)
candidates = []
if try_ext:
candidates.append(try_ext)
candidates.extend([".jpg", ".png", ".txt", ".bin"])
if ext and len(ext) <= 5 and ext not in candidates:
candidates.insert(0, ext)
tried = set()
for c in candidates:
try_name = name_root + c
if try_name in tried:
continue
tried.add(try_name)
try:
r = await api.upload_file(local_path, try_name, folder_id)
if isinstance(r, dict):
fid = r.get("id") or (r.get("raw") or {}).get(
"createdFile", {}
).get("id")
if fid:
return str(fid)
except Exception:
continue
return None
</code_context>
<issue_to_address>
**issue (code-quality):** Use named expression to simplify assignment and conditional [×2] ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
</issue_to_address>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We failed to fetch pull request #2986.
You can try again by commenting this pull request with @sourcery-ai review
, or contact us for help.
文件上传接口在近日继续维护更新,优化代码,暂时不建议合并。 |
维护完成,重构和精简代码完成。 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enhances the Misskey adapter with media upload support, richer note creation options (cw, poll, renote, channel), and improved WebSocket handling and retry logic.
- Adds concurrent file upload with fallback-from-URL and local upload; introduces configurable upload/download options.
- Improves WebSocket auto-resubscription, smarter retry with backoff + jitter, and better logging.
- Extends APIs and utilities (visibility resolution, poll formatting, unified send with media).
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
File | Description |
---|---|
astrbot/core/platform/sources/misskey/misskey_utils.py | Adds helpers for file ID extraction, payload building, visibility parsing refactor (with backward-compatible alias), poll formatting, and component URL/path resolution. |
astrbot/core/platform/sources/misskey/misskey_event.py | Routes event sending through adapter’s send_by_session (to leverage upload), improves fallback path formatting and logging. |
astrbot/core/platform/sources/misskey/misskey_api.py | Adds smarter retry decorator, new file management APIs, upload-from-URL with fallback to local, unified media sending, and richer create_note parameters; improves WebSocket handling and logging. |
astrbot/core/platform/sources/misskey/misskey_adapter.py | Wires new config, registers handlers, implements concurrent upload pipeline, poll embedding, visibility handling, and unified sending across chat/room/note. |
astrbot/core/config/default.py | Exposes new Misskey upload/download and safety settings in default config and UI metadata. |
async def _get_str_value(coro_or_val): | ||
"""辅助函数:统一处理协程或普通值""" | ||
try: | ||
if hasattr(coro_or_val, "__await__"): | ||
result = await coro_or_val | ||
else: | ||
result = coro_or_val | ||
return result if isinstance(result, str) else None | ||
except Exception: | ||
return None |
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use inspect.isawaitable to detect awaitables rather than checking await directly; it covers more cases (coroutines, Futures, Tasks).
Copilot uses AI. Check for mistakes.
…k memory usage. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Motivation / 动机
本次更新对 Misskey 适配器进行了大量功能更新和修复问题。
Modifications / 改动点
Verification Steps / 验证步骤
Screenshots or Test Results / 运行截图或测试结果
Compatibility & Breaking Changes / 兼容性与破坏性变更
Checklist / 检查清单
requirements.txt
和pyproject.toml
文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations inrequirements.txt
andpyproject.toml
.Sourcery 总结
更新 Misskey 适配器以支持带有重试和回退机制的并发文件上传,丰富笔记创建功能,增加 cw、投票、转推和频道字段,改进 WebSocket 处理和重试逻辑,并扩展 API 方法以支持灵活的负载选项。
新功能:
改进:
retry_async
装饰器文档:
Original summary in English
Summary by Sourcery
Update Misskey adapter to support concurrent file uploads with retry and fallback mechanisms, enrich note creation with cw, poll, renote, and channel fields, improve WebSocket handling and retry logic, and extend API methods with flexible payload options.
New Features:
Enhancements:
Documentation:
Sourcery 总结
更新 Misskey 适配器,增加了大量功能、增强和修复:实现了强大的文件上传工作流,丰富了笔记创建字段,改进了 WebSocket 处理和重试逻辑,并暴露了新的配置选项。
新功能:
错误修复:
增强功能:
文档:
Original summary in English
Summary by Sourcery
Update Misskey adapter with extensive feature additions, enhancements, and fixes: implement robust file upload workflows, enrich note creation fields, improve WebSocket handling and retry logic, and expose new configuration options.
New Features:
Bug Fixes:
Enhancements:
Documentation: