fix(ingest): raise ref cap to 50 and clarify invalid bvid error

This commit is contained in:
kairee committed 2026-11-01 15:05:17 +08:00
1 parent b5bf7d9b8f
commit b0c53e8a2f
7 files changed
+107 -9

No files matched your search

+1 -1
View File
@@ -489,7 +489,7 @@ P3 实现说明:
- `callback_data` 使用 `add:<aid>` / `ign:<aid>` / `blk:<aid>`(`fav:` 兼容旧消息);
- 入库只写本地库;`video` 的 `removed` / `dropped` / `unfaved` 状态会被重新激活;
- 引用解析是纯函数 `video_ref.extract_video_refs`,`b23.tv` 短链由 `bilibili.client.resolve_short_link` 跟随跳转;
- 手动入库不做「标题含咻咻满」等规则校验;单条消息的引用数量有上限;
- 手动入库不做「标题含咻咻满」等规则校验;单条消息最多处理 50 个引用,超出会在回执中提示分批发送;无效 BV/av 号(`-400`)逐条报错、不中断其余引用;
- Telegram 只接受 `chat_id` 与 `from.id` 都等于配置私聊用户的回调与消息。
---
+3
View File
@@ -80,6 +80,9 @@ def format_manual_reply(manual_result: Any) -> str:
return "未识别到 B 站视频引用,请发送视频链接或 BV 号,例如 BV1x8x166EzY。"
results = list(getattr(manual_result, "results", []) or [])
lines = [_result_line(item) for item in results]
truncated = int(getattr(manual_result, "truncated", 0) or 0)
if truncated > 0:
lines.append(f"⚠️ 本消息还有 {truncated} 个引用未处理,请分批再发一次。")
header = f"共识别 {refs} 个引用,入库 {int(getattr(manual_result, 'added', 0) or 0)} 条:"
return "\n".join([header, *lines])
+25 -5
View File
@@ -11,7 +11,11 @@ import time
from dataclasses import dataclass, field
from typing import Any
from ..bilibili.client import BilibiliError, BilibiliProtocolError
from ..bilibili.client import (
BilibiliAPIError,
BilibiliError,
BilibiliProtocolError,
)
from ..config import ConfigStore
from ..repository import Repository
from ..timeutil import shanghai_date
@@ -42,6 +46,7 @@ class IngestResult:
class ManualIngestResult:
refs: int = 0
added: int = 0
truncated: int = 0
results: list[IngestResult] = field(default_factory=list)
@property
@@ -52,6 +57,7 @@ class ManualIngestResult:
return {
"refs": self.refs,
"added": self.added,
"truncated": self.truncated,
"ok": self.ok,
"results": [result.as_dict() for result in self.results],
}
@@ -180,16 +186,30 @@ class LibraryService:
# ------------------------------------------------------------ manual
def ingest_text(self, text: object) -> ManualIngestResult:
refs = extract_video_refs(text, limit=self.max_refs)
result = ManualIngestResult(refs=len(refs))
if not refs:
all_refs = extract_video_refs(text, limit=None)
result = ManualIngestResult(refs=len(all_refs))
if not all_refs:
return result
if len(all_refs) > self.max_refs:
result.truncated = len(all_refs) - self.max_refs
all_refs = all_refs[: self.max_refs]
result.refs = len(all_refs)
client = self.read_provider.get()
ups: list[tuple[int | None, str]] = []
for ref in refs:
for ref in all_refs:
try:
detail = self._resolve_ref(client, ref)
except BilibiliAPIError as exc:
message = (
"无法解析该视频引用:BV 号/av 号无效或视频不存在(code=-400)"
if exc.code == -400 else str(exc)
)
result.results.append(IngestResult(
ok=False, aid=None, state="error", message=message,
))
self._log("warning", "ingest.manual_failed", error=str(exc))
continue
except BilibiliError as exc:
result.results.append(IngestResult(
ok=False, aid=None, state="error", message=str(exc),
+2 -1
View File
@@ -25,7 +25,8 @@ _AID_RE = re.compile(r"(?<![0-9A-Za-z])av(\d{1,20})(?![0-9A-Za-z])", re.IGNORECA
_SHORT_RE = re.compile(r"b23\.tv/([0-9A-Za-z]+)")
# 单条消息最多处理的引用数,避免误粘贴一段文本导致突发请求。
DEFAULT_MAX_REFS = 10
# 15 个 BV 号批量测试手动入库是常见用法,上限放宽到 50,超出时回执会提示分批发送。
DEFAULT_MAX_REFS = 50
@dataclass(frozen=True)
+49 -2
View File
@@ -13,7 +13,10 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.stdout.reconfigure(encoding="utf-8")
from fixtures import view_for # noqa: E402
from reaction_board.bilibili.client import BilibiliError # noqa: E402
from reaction_board.bilibili.client import ( # noqa: E402
BilibiliAPIError,
BilibiliError,
)
from reaction_board.bilibili.models import VideoDetail # noqa: E402
from reaction_board.config import ConfigStore # noqa: E402
from reaction_board.database import Database # noqa: E402
@@ -51,14 +54,18 @@ def detail(
class FakeReadClient:
def __init__(self, by_bvid=None, by_aid=None, short_map=None, fail_bvids=()):
def __init__(self, by_bvid=None, by_aid=None, short_map=None, fail_bvids=(),
api_errors=None):
self.by_bvid = by_bvid or {}
self.by_aid = by_aid or {}
self.short_map = short_map or {}
self.fail_bvids = set(fail_bvids)
self.api_errors = dict(api_errors or {})
def view_detail(self, *, aid=None, bvid=None):
if bvid is not None:
if bvid in self.api_errors:
raise BilibiliAPIError(self.api_errors[bvid], "请求错误")
if bvid in self.fail_bvids:
raise BilibiliError(f"view 失败: {bvid}")
if bvid in self.by_bvid:
@@ -263,6 +270,44 @@ def test_snapshot_not_overwritten():
db.close()
def test_manual_ingest_truncation():
with tempfile.TemporaryDirectory() as tmp:
bvs = {
"BV1x8x166E00": detail(1, bvid="BV1x8x166E00"),
"BV1x8x166E01": detail(2, bvid="BV1x8x166E01"),
"BV1x8x166E02": detail(3, bvid="BV1x8x166E02"),
}
client = FakeReadClient(by_bvid=bvs)
store = ConfigStore(write_config(tmp), env={})
db = Database(Path(tmp) / "app.db", backup_dir=Path(tmp) / "backup")
db.initialize()
try:
repo = Repository(db, clock=lambda: NOW)
service = LibraryService(
store, repo, FakeProvider(client), clock=lambda: NOW, max_refs=2
)
result = service.ingest_text(" ".join(bvs))
assert result.refs == 2
assert result.truncated == 1
assert result.added == 2
assert len(result.results) == 2
finally:
db.close()
def test_manual_ingest_invalid_bvid_message():
with tempfile.TemporaryDirectory() as tmp:
client = FakeReadClient(api_errors={"BV1x8x166E00": -400})
db, repo, service = setup(tmp, client)
try:
result = service.ingest_text("BV1x8x166E00")
assert result.refs == 1
assert result.results[0].state == "error"
assert "无效或视频不存在" in result.results[0].message
finally:
db.close()
def test_ignore_and_blacklist():
with tempfile.TemporaryDirectory() as tmp:
client = FakeReadClient()
@@ -289,6 +334,8 @@ def main():
test_manual_ingest_forms_and_multiple()
test_manual_ingest_error_isolation()
test_snapshot_not_overwritten()
test_manual_ingest_truncation()
test_manual_ingest_invalid_bvid_message()
test_ignore_and_blacklist()
print("OK: 入库、手动入库、忽略、拉黑测试全部通过")
+15
View File
@@ -15,6 +15,7 @@ from reaction_board.notify.telegram import ( # noqa: E402
TelegramClient,
TelegramError,
format_duration,
format_manual_reply,
)
from reaction_board.services.library import ( # noqa: E402
ActionResult,
@@ -51,6 +52,19 @@ def test_format_duration():
assert format_duration(3725) == "1:02:05"
def test_manual_reply_truncation_notice():
result = ManualIngestResult(
refs=50, added=1, truncated=2,
results=[IngestResult(True, 1, "added", title="t", message="已入库")],
)
text = format_manual_reply(result)
assert "共识别 50 个引用" in text
assert "还有 2 个引用未处理" in text
assert format_manual_reply(ManualIngestResult()) == (
"未识别到 B 站视频引用,请发送视频链接或 BV 号,例如 BV1x8x166EzY。"
)
def test_send_candidates_and_chunking():
transport = FakeTransport()
bot = TelegramClient("token", 42, transport=transport)
@@ -300,6 +314,7 @@ def test_poller_backoff_and_recovery():
def main():
test_format_duration()
test_manual_reply_truncation_notice()
test_send_candidates_and_chunking()
test_send_candidates_dedupes_by_aid()
test_get_updates_and_answer()
+12
View File
@@ -63,6 +63,17 @@ def test_limit():
assert len(extract_video_refs(text, limit=None)) == 3
def test_default_cap_allows_bulk_paste():
from reaction_board.video_ref import DEFAULT_MAX_REFS
assert DEFAULT_MAX_REFS >= 15
text = " ".join(f"BV1x8x166E{i:02d}" for i in range(15))
assert len(extract_video_refs(text)) == 15
assert len(extract_video_refs(" ".join(
f"BV1x8x166E{i:02d}" for i in range(DEFAULT_MAX_REFS + 5)
))) == DEFAULT_MAX_REFS
def test_invalid_and_boundaries():
assert extract_video_refs("") == []
assert extract_video_refs(None) == []
@@ -85,6 +96,7 @@ def main():
test_short_link()
test_multiple_and_order_and_dedup()
test_limit()
test_default_cap_allows_bulk_paste()
test_invalid_and_boundaries()
print("OK: 视频引用(BV/av/URL/短链)解析测试全部通过")