Skip to content

Commit 7b993ee

Browse files
committed
store all msg_ids for multi-part messages in drivers to ensure accurate reply bridging
- fix (qq): split voice / video segment when sending - fix(qq): defer file uploads after sending text to prevent files arriving before text - update deps - docker workflow now will not triggered by PRs - merge starting log
1 parent bfc2323 commit 7b993ee

9 files changed

Lines changed: 540 additions & 490 deletions

File tree

.github/workflows/docker.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ on:
99
- ".github/**"
1010
- "docs/**"
1111
- "**/*.md"
12-
pull_request:
13-
branches:
14-
- main
15-
- 0.*-dev
12+
# pull_request:
13+
# branches:
14+
# - main
15+
# - 0.*-dev
1616
workflow_dispatch:
1717

1818
env:

drivers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ async def start(self):
4040
Long-running drivers should loop indefinitely here."""
4141

4242
@abstractmethod
43-
async def send(self, channel: dict, text: str, **kwargs) -> str | None:
43+
async def send(self, channel: dict, text: str, **kwargs) -> str | list[str] | None:
4444
"""Send *text* to the given *channel* on this platform."""

drivers/feishu.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ async def send(
422422
return None
423423

424424
reply_to_id = kwargs.get("reply_to_id")
425-
first_msg_id = None
425+
msg_ids = []
426426

427427
rich_header = kwargs.get("rich_header")
428428
if rich_header:
@@ -440,8 +440,8 @@ async def send(
440440
mid = await self._send_feishu_msg(
441441
chat_id, "text", json.dumps({"text": text}), reply_to_id
442442
)
443-
if not first_msg_id:
444-
first_msg_id = mid
443+
if mid:
444+
msg_ids.append(mid)
445445

446446
source_proxy = self._source_proxy_from_kwargs(kwargs)
447447

@@ -459,8 +459,8 @@ async def send(
459459
json.dumps({"text": f"[{att.type.capitalize()}: {label}]"}),
460460
reply_to_id,
461461
)
462-
if not first_msg_id:
463-
first_msg_id = mid
462+
if mid:
463+
msg_ids.append(mid)
464464
continue
465465

466466
data_bytes, mime = result
@@ -472,36 +472,36 @@ async def send(
472472
mid = await self._send_feishu_msg(
473473
chat_id, "image", json.dumps({"image_key": key}), reply_to_id
474474
)
475-
if not first_msg_id:
476-
first_msg_id = mid
475+
if mid:
476+
msg_ids.append(mid)
477477
else:
478478
mid = await self._send_feishu_msg(
479479
chat_id,
480480
"text",
481481
json.dumps({"text": f"[Image: {fname}]"}),
482482
reply_to_id,
483483
)
484-
if not first_msg_id:
485-
first_msg_id = mid
484+
if mid:
485+
msg_ids.append(mid)
486486
else:
487487
key = await self._upload_file(data_bytes, fname)
488488
if key:
489489
mid = await self._send_feishu_msg(
490490
chat_id, "file", json.dumps({"file_key": key}), reply_to_id
491491
)
492-
if not first_msg_id:
493-
first_msg_id = mid
492+
if mid:
493+
msg_ids.append(mid)
494494
else:
495495
mid = await self._send_feishu_msg(
496496
chat_id,
497497
"text",
498498
json.dumps({"text": f"[{att.type.capitalize()}: {fname}]"}),
499499
reply_to_id,
500500
)
501-
if not first_msg_id:
502-
first_msg_id = mid
501+
if mid:
502+
msg_ids.append(mid)
503503

504-
return first_msg_id
504+
return msg_ids if msg_ids else None
505505

506506
async def _send_feishu_msg(
507507
self, chat_id: str, msg_type: str, content: str, reply_to_id: str | None = None

drivers/qq.py

Lines changed: 90 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1913,12 +1913,19 @@ async def send(
19131913
return None
19141914

19151915
segments: list[dict] = []
1916+
msg_ids: list[str] = []
1917+
deferred_file_uploads = []
19161918

19171919
reply_to_id = kwargs.get("reply_to_id")
19181920
if reply_to_id:
19191921
segments.append({"type": "reply", "data": {"id": str(reply_to_id)}})
19201922

19211923
rich_header = kwargs.get("rich_header")
1924+
# Video/Record cannot be mixed with text in QQ, so if there are such attachments,
1925+
# we still want to make sure the header goes with the text, but the whole text
1926+
# message must be sent separately from the video/record message.
1927+
# We will handle the separation later, so we just prepend the header to the text if there is text.
1928+
# If there is NO text but there ARE non-image attachments, we send the header separately.
19221929
has_non_image_attachments = any(
19231930
att.type != "image"
19241931
for att in (attachments or [])
@@ -1947,6 +1954,10 @@ async def send(
19471954
f"NapCat [{self.instance_id}] failed to send standalone rich header "
19481955
f"before media message: {header_resp}"
19491956
)
1957+
else:
1958+
data = header_resp.get("data") or {}
1959+
if "message_id" in data:
1960+
msg_ids.append(str(data["message_id"]))
19501961

19511962
# Process mentions: replace @Name with at segments
19521963
mentions = kwargs.get("mentions", [])
@@ -2063,50 +2074,44 @@ async def send(
20632074
if result:
20642075
data_bytes, _ = result
20652076
fname = att.name or "file"
2066-
if self._supports_stream_file_upload():
2067-
mode = self._resolve_send_mode(len(data_bytes))
2068-
if mode == "base64":
2069-
b64 = base64.b64encode(data_bytes).decode()
2070-
await self._call(
2071-
"upload_group_file",
2072-
{
2073-
"group_id": int(group_id),
2074-
"file": f"base64://{b64}",
2075-
"name": fname,
2076-
},
2077-
)
2078-
else: # stream (default)
2079-
file_path = await self._upload_file_stream(
2080-
data_bytes, fname
2081-
)
2082-
if file_path:
2077+
2078+
async def _do_upload(d=data_bytes, fn=fname, gid=group_id):
2079+
if self._supports_stream_file_upload():
2080+
mode = self._resolve_send_mode(len(d))
2081+
if mode == "base64":
2082+
b64 = base64.b64encode(d).decode()
20832083
await self._call(
20842084
"upload_group_file",
20852085
{
2086-
"group_id": int(group_id),
2087-
"file": file_path,
2088-
"name": fname,
2086+
"group_id": int(gid),
2087+
"file": f"base64://{b64}",
2088+
"name": fn,
20892089
},
20902090
)
2091-
else:
2092-
segments.append(
2093-
{
2094-
"type": "text",
2095-
"data": {"text": f"\n[文件: {att.name}]"},
2096-
}
2091+
else: # stream (default)
2092+
file_path = await self._upload_file_stream(
2093+
d, fn
20972094
)
2098-
else:
2099-
if not await self._upload_group_file_from_bytes(
2100-
data_bytes,
2101-
fname,
2102-
str(group_id),
2103-
):
2104-
segments.append(
2105-
{
2106-
"type": "text",
2107-
"data": {"text": f"\n[文件: {att.name}]"},
2108-
}
2109-
)
2095+
if file_path:
2096+
await self._call(
2097+
"upload_group_file",
2098+
{
2099+
"group_id": int(gid),
2100+
"file": file_path,
2101+
"name": fn,
2102+
},
2103+
)
2104+
else:
2105+
await self._call("send_group_msg", {"group_id": int(gid), "message": [{"type": "text", "data": {"text": f"\n[文件发送失败: {fn}]"}}]})
2106+
else:
2107+
if not await self._upload_group_file_from_bytes(
2108+
d,
2109+
fn,
2110+
str(gid),
2111+
):
2112+
await self._call("send_group_msg", {"group_id": int(gid), "message": [{"type": "text", "data": {"text": f"\n[文件发送失败: {fn}]"}}]})
2113+
2114+
deferred_file_uploads.append(_do_upload)
21102115
else:
21112116
segments.append(
21122117
{
@@ -2115,20 +2120,54 @@ async def send(
21152120
}
21162121
)
21172122

2118-
if not segments:
2119-
return None
2123+
main_segments = []
2124+
standalone_segments = []
2125+
for seg in segments:
2126+
if seg["type"] in ("video", "record"):
2127+
standalone_segments.append(seg)
2128+
else:
2129+
main_segments.append(seg)
21202130

2121-
resp = await self._call(
2122-
"send_group_msg",
2123-
{
2124-
"group_id": int(group_id),
2125-
"message": segments,
2126-
},
2127-
)
2128-
if resp and resp.get("status") == "ok":
2129-
data = resp.get("data") or {}
2130-
return str(data.get("message_id", ""))
2131-
return None
2131+
if main_segments:
2132+
if (
2133+
len(main_segments) == 1
2134+
and main_segments[0]["type"] == "reply"
2135+
and standalone_segments
2136+
):
2137+
# If only reply segment remains, attach it to the first standalone segment
2138+
standalone_segments[0] = [main_segments[0], standalone_segments[0]]
2139+
main_segments = []
2140+
else:
2141+
resp = await self._call(
2142+
"send_group_msg",
2143+
{
2144+
"group_id": int(group_id),
2145+
"message": main_segments,
2146+
},
2147+
)
2148+
if resp and resp.get("status") == "ok":
2149+
data = resp.get("data") or {}
2150+
if "message_id" in data:
2151+
msg_ids.append(str(data["message_id"]))
2152+
2153+
for seg in standalone_segments:
2154+
msg_to_send = seg if isinstance(seg, list) else [seg]
2155+
resp = await self._call(
2156+
"send_group_msg",
2157+
{
2158+
"group_id": int(group_id),
2159+
"message": msg_to_send,
2160+
},
2161+
)
2162+
if resp and resp.get("status") == "ok":
2163+
data = resp.get("data") or {}
2164+
if "message_id" in data:
2165+
msg_ids.append(str(data["message_id"]))
2166+
2167+
for upload_func in deferred_file_uploads:
2168+
await upload_func()
2169+
2170+
return msg_ids if msg_ids else None
21322171

21332172

21342173
register("qq", QqConfig, QqDriver)

drivers/telegram.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ async def send(
542542
except (ValueError, TypeError):
543543
pass
544544

545-
first_msg_id = None
545+
msg_ids = []
546546

547547
rich_header = kwargs.get("rich_header")
548548
if rich_header:
@@ -668,8 +668,8 @@ async def send(
668668
reply_parameters=reply_params,
669669
)
670670

671-
if not first_msg_id:
672-
first_msg_id = str(sent.message_id)
671+
if sent and sent.message_id:
672+
msg_ids.append(str(sent.message_id))
673673
caption_used = True
674674
except Exception as e:
675675
label = att.name or att.url or fname
@@ -688,10 +688,10 @@ async def send(
688688
link_preview_options=link_preview_opts,
689689
reply_parameters=reply_params,
690690
)
691-
if not first_msg_id:
692-
first_msg_id = str(sent.message_id)
691+
if sent and sent.message_id:
692+
msg_ids.append(str(sent.message_id))
693693

694-
return first_msg_id
694+
return msg_ids if msg_ids else None
695695

696696
except Exception as e:
697697
logger.error(f"Telegram [{self.instance_id}] send failed: {e}")

drivers/vocechat.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ async def send(
281281
)
282282

283283
reply_to_id = kwargs.get("reply_to_id")
284-
first_msg_id = None
284+
msg_ids = []
285285

286286
rich_header = kwargs.get("rich_header")
287287
if rich_header:
@@ -308,8 +308,8 @@ async def send(
308308
# Use markdown if rich_header was applied; plain text otherwise
309309
ct = "text/markdown" if rich_header else "text/plain"
310310
mid = await self._post_message(endpoint, text.encode(), ct, reply_to_id)
311-
if not first_msg_id:
312-
first_msg_id = mid
311+
if mid:
312+
msg_ids.append(mid)
313313

314314
source_proxy = self._source_proxy_from_kwargs(kwargs)
315315
for att in attachments or []:
@@ -326,8 +326,8 @@ async def send(
326326
"text/plain",
327327
reply_to_id,
328328
)
329-
if not first_msg_id:
330-
first_msg_id = mid
329+
if mid:
330+
msg_ids.append(mid)
331331
continue
332332

333333
data_bytes, mime = result
@@ -338,8 +338,8 @@ async def send(
338338
mid = await self._post_message(
339339
endpoint, body, "vocechat/file", reply_to_id
340340
)
341-
if not first_msg_id:
342-
first_msg_id = mid
341+
if mid:
342+
msg_ids.append(mid)
343343
else:
344344
label = att.name or fname
345345
mid = await self._post_message(
@@ -348,10 +348,10 @@ async def send(
348348
"text/plain",
349349
reply_to_id,
350350
)
351-
if not first_msg_id:
352-
first_msg_id = mid
351+
if mid:
352+
msg_ids.append(mid)
353353

354-
return first_msg_id
354+
return msg_ids if msg_ids else None
355355

356356
async def _post_message(
357357
self, url: str, body: bytes, content_type: str, reply_to_id: str | None = None

drivers/yunhu.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ async def send(
286286

287287
chat_type: str = channel.get("chat_type", "group")
288288
reply_to_id = kwargs.get("reply_to_id")
289-
first_msg_id = None
289+
msg_ids = []
290290

291291
rich_header = kwargs.get("rich_header")
292292
if rich_header:
@@ -411,8 +411,8 @@ def _add_common(p):
411411
or d.get("messageInfo", {}).get("messageId")
412412
)
413413

414-
if mid and not first_msg_id:
415-
first_msg_id = str(mid)
414+
if mid:
415+
msg_ids.append(str(mid))
416416
else:
417417
logger.error(
418418
f"Yunhu [{self.instance_id}] send failed API error: {data}"
@@ -426,7 +426,7 @@ def _add_common(p):
426426
except Exception as e:
427427
logger.error(f"Yunhu [{self.instance_id}] send failed: {e}")
428428

429-
return first_msg_id
429+
return msg_ids if msg_ids else None
430430

431431

432432
register("yunhu", YunhuConfig, YunhuDriver)

0 commit comments

Comments
 (0)