Commit Graph
185 Commits
Author SHA1 Message Date
DrSmoothl ebd02c3f3d chore(release): v1.12.3 2026-07-15 21:31:59 +08:00
DrSmoothl 9c61d991bd feat(onebot): report stalled receive health
Separate account login state from receive-path health in get_status. A logged-in account remains online while good becomes false only after the receive path has been confirmed stale.

Apply the same health value to OneBot lifecycle and heartbeat events so API polling and event consumers observe consistent status semantics.

Regenerate the MCP action catalog with the clarified contract and add regression coverage for the online-but-degraded response.

Refs #233.
2026-07-15 16:48:02 +08:00
DrSmoothl 2fea3ca4d9 feat(core): validate backup restores before mutation
Route selected backup entries through owner-owned config parsers before any filesystem write. Reject unsupported paths, mismatched encodings, malformed base64, lossy normalization, invalid effective overlays, and decoded payloads larger than 32 MiB.

Treat WebUI state, private keys, and every OneBot config as a single sensitive group. Skip unselected sensitive entries without inspecting their contents, preserve recognized migrations, and assign restrictive modes to credential files.
2026-07-15 16:11:44 +08:00
DrSmoothl a3467a8d89 chore(release): v1.12.2 2026-07-14 07:57:08 +08:00
DrSmoothl 5885187e7d test(group-notice): cover publishing options and readback
Verify endpoint selection, option validation, image metadata parsing, response merging, and OneBot forwarding. Cover the type-20 new-member path and fail-fast conflict behavior.
2026-07-14 03:39:27 +08:00
DrSmoothl 51eb1f733d feat(group-notice): support advanced announcement controls
Route regular and new-member announcements through their correct web endpoints. Validate option conflicts, preserve existing defaults, merge returned notice collections, and surface transport and upload failures.
2026-07-14 03:39:11 +08:00
DrSmoothl defe15694b feat(onebot): implement mark all as read
Replace the compatibility no-op with a real batched read action for current groups and observed incoming private conversations.

Exclude stale groups, group temporary sessions, and sent-message echoes from private targets, then expose the functional action through the generated MCP catalog.

Add action and message-store regressions for target selection and delegation.
2026-07-13 23:02:08 +08:00
DrSmoothl 03aec3b052 fix(core): verify conversation read updates
Replace message-header sequence forwarding with a server-guided read flow for group and private conversations.

Batch read reports conservatively, validate per-conversation results and cursor advancement, and fail with actionable errors when the server does not apply an update.

Add regression coverage for mixed batches, pagination, already-current sessions, malformed responses, and private sequence-domain mismatches.
2026-07-13 23:01:39 +08:00
DrSmoothl b5452c59e6 fix(onebot): return categorized friend lists 2026-07-13 18:51:45 +08:00
DrSmoothl 2ae9a47249 fix(onebot): clarify invalid JSON segment errors
Validate the public OneBot data.data field before converting JSON segments to the internal message element shape. Invalid payloads still return retcode 1400, but now identify the correct public field without reaching the QQ bridge.

Add Action-level regression coverage for issue #220.
2026-07-12 16:43:05 +08:00
DrSmoothl a824e0b46a fix(onebot): repair group system message queries 2026-07-10 19:11:15 +08:00
DrSmoothl b53004ee23 fix(proton): accept sign-extended uint32 field values 2026-07-10 18:20:11 +08:00
DrSmoothl c04864dd21 chore(release): v1.12.1 2026-07-10 14:00:40 +08:00
DrSmoothl 852004291b feat(onebot): reconcile and drain network adapters
Serialize desired network plans, isolate per-adapter apply failures, restore prior bindings when possible, and report saved versus applied state. Quiesce Action and event ingress, await in-flight dispatches and transport closes, retain failed generations for retry, and propagate shutdown failures.
2026-07-10 13:49:17 +08:00
DrSmoothl 01688c7567 fix(protocol): enforce message element contracts end to end
Replace the open message bag with a validated discriminated union and preflight complete messages before uploads or sends. Return BAD_REQUEST for invalid or unsendable segments instead of dropping fields, sending partial content, or silently changing private messages.
2026-07-10 13:47:06 +08:00
DrSmoothl aff6ad9179 refactor(onebot): make config persistence explicit
Persist user saves as authoritative per-account snapshots while retaining legacy overlay compatibility. Validate deterministic topology errors before writing and replace configuration files atomically.
2026-07-10 13:46:09 +08:00
DrSmoothl 588fdab8de refactor(onebot): compile and seal the action registry
Compile canonical names, aliases, stream kinds, documentation, and raw reservations into one immutable registry. Reject duplicate, undeclared, mismatched, or late handler registration before requests can be dispatched.
2026-07-10 13:45:32 +08:00
DrSmoothl 4825434ffa fix(onebot): await malformed stream responses
Wait for asynchronous transports to flush bad-request frames before processStreamRequest returns. This prevents malformed Stream API requests from losing their terminal error response.
2026-07-10 13:43:31 +08:00
DrSmoothl 35958a7fc2 fix(onebot): use canonical group request flags 2026-07-10 02:42:07 +08:00
DrSmoothl 3c73d1d5b0 feat(onebot): reply into group temp sessions (临时会话) (#212)
send_private_msg / send_msg can now reply to a group temp-session message
by passing the source group_id. Inbound temp messages surface their
source group on sender.group_id; a reply is only made into a temp session
the bot has an inbound record of. Group temp replies route through
RoutingHead.grpTmp (field 3).
2026-07-10 02:29:18 +08:00
DrSmoothl 5e73d69663 fix(test): add groupName to GroupMessage test fixtures (#212 CI)
core's typecheck covers its own tests (protocol/onebot typecheck src
only), so the newly-required GroupMessage.groupName field failed the
Dev Build typecheck on test fixtures that construct group_message
events. Add groupName to every such fixture across core/onebot/protocol
tests.
2026-07-09 19:18:09 +08:00
DrSmoothl 9b470d0412 feat(onebot): add group_name to group message events (#212)
Group message events (message + message_sent) now carry group_name — a
non-standard but common OneBot extension. The name comes from the
message's own wire field (ResponseGrp field 7, accurate even right after
a rename), falling back to the identity group cache, else '' when neither
is available. It's resolved in the group-message decoder, where both the
wire head and the identity cache are already at hand, so the converter
just passes it through (no ConverterContext change).

Verified live (tempserver): real inbound group messages from a non-self
account carry group_name set to the actual group name.
2026-07-09 18:50:54 +08:00
DrSmoothl b6fb5bd530 chore(release): v1.12.0 2026-07-07 22:38:02 +08:00
DrSmoothl 8fbcbd29ff feat(onebot): honour a custom per-node time in merged forward (#209)
Merged-forward nodes always stamped the current time. A custom forward can now
set each node's displayed timestamp via the OneBot `data.time` field (unix
seconds), matching the field the wire already carries per node.

parseForwardNodes reads `data.time` on custom/fake nodes and threads it onto the
node payload (the `time?` field already existed for the receive side);
buildForwardPushBody uses it for `contentHead.timestamp`, falling back to now
when absent. The value is validated to a plausible unix-seconds range — the wire
field is uint32, so a millisecond or out-of-range input is rejected (would
overflow) and defaults to now rather than corrupting the timestamp. The
message_id-reference branch (time from the quoted message) and the c2c-file
expireTime (real now) are unchanged. Nested inner nodes honour their own time.
2026-07-07 21:35:33 +08:00
DrSmoothl 1914807770 fix(onebot): apply the transport heartbeat to the reverse-WS server too
Symmetric with the ws-client fix (#208): the reverse-WS server now pings each
attached client and terminates one that goes fully silent for ~90s (2 unanswered
pings), reaping a half-open client that would otherwise linger in the connection
set. Reuses startHeartbeat with the same conservative, activity-reset tolerance,
so a client with live traffic is never dropped. The per-connection stop is kept
on the ForwardConn entry and cleared in both close() and the socket 'close'
handler; no shared instance state, so the #97 overlap does not apply here.
2026-07-07 21:20:19 +08:00
DrSmoothl c09c6e0883 fix(onebot): heal half-open reverse-WS client with a transport heartbeat
A reverse-WS client could end up half-open after the peer restarted: still
writable (events kept flowing out) but no longer readable, so inbound action
frames silently never arrived and only a full restart recovered it (#208). The
OneBot app-level meta heartbeat is a one-way push and cannot detect this.

Add an RFC6455 ping/pong keepalive (startHeartbeat) wired into the ws-client
adapter. It pings every 30s and declares the link dead only after 2 unanswered
pings (~90s of total silence), then terminates the socket so the existing
reconnect path runs. Any inbound activity — a pong OR a message — resets the
counter, so a connection carrying real traffic is never reaped; the conservative
tolerance keeps the false-positive rate near zero (a GC pause or latency spike is
absorbed). The stop is per-socket and also held on the instance so close()/reload
halt it immediately rather than waiting for the deferred 'close'; a symmetric
identity guard on 'open' mirrors the #97 stale-socket guard on 'close'.
2026-07-07 21:20:07 +08:00
DrSmoothl d9dfe513ee feat(onebot): report forced offline as notice/bot_offline
Register a handler for the StatusService.KickNT push (QQ NT's unified
forced-offline notification — kick / login-elsewhere / risk-control),
decode KickNTResponse and emit bot_offline {user_id, tag, message},
matching NapCat. Command + field layout (f4=tips_title, f3=tips_content)
RE-confirmed against wrapper.linux.node + Lagrange; not yet live-captured.
2026-07-07 15:25:35 +08:00
DrSmoothl 447b40a259 feat(onebot): report profile like as notify/profile_like
Decode the multiplexed 0x210 subType-39 push: when body.msgContent is a
ProfileLikeTip with inner msgType==0/subType==203, emit notify/profile_like
{operator_id, operator_nick, times}. times is parsed from detail.txt.
Proto shape from NapCat's raw decoder; wiring + decode verified, but the
byte-exact wire is pending a real like capture to confirm.
2026-07-07 03:05:14 +08:00
DrSmoothl 0d4d58605f feat(onebot): report group special title grant as notify/title
Decode the 0x2DC subType-16 field13==6 push: eventParam carries the member
uin (f5) and a gray-tip template (f2) whose last rich token holds the title
text. Emit notify/title {group_id, user_id, title}, matching NapCat. Field
layout taken from a real on-wire sample, not ported blindly.
2026-07-07 02:53:38 +08:00
DrSmoothl d3d1d2512e feat(onebot): report group member card change as notice/group_card
Detect a group member's card change from message traffic (sender card vs
cached roster card) and emit group_card {group_id, user_id, card_new,
card_old}, matching NapCat's parseCardChangedEvent. Fires only on a real
change of a known member; first-contact and unchanged cards are suppressed.
2026-07-07 02:23:17 +08:00
DrSmoothl bfde0daab4 feat(onebot): report group name change as notify/group_name
Dispatch the 0x2DC subType-16 push by NotifyMessageBody.field13 (was
emoji-react only); field13==12 decodes eventParam as GroupNameChange and
emits notify/group_name {group_id, user_id=operator, name_new}, matching
NapCat. Emoji-react (field13==35) path unchanged.
2026-07-07 02:05:52 +08:00
DrSmoothl 852870db59 feat(onebot): report C2C input status as notify/input_status
Decode the 0x210/0x115 input-state sys_msg (RE'd from wrapper.linux.node
aio_input_state_worker) into a friend_input_status event and map it to a
notify/input_status notice, matching NapCat's onInputStatusPush.
event_type 1 = typing, 3 = recording voice; status_text synthesised to
match the client's own strings.
2026-07-07 01:26:31 +08:00
DrSmoothl d35e276730 chore(release): v1.11.4 2026-07-06 15:04:55 +08:00
DrSmoothl b6107e38d8 fix(onebot): default forward node sender to self when user_id is 0/absent
A fake forward node (send_group_forward_msg / send_private_forward_msg) whose
data.user_id was "0" or omitted got rejected with "forward node user_id/uin is
required", even though the protocol端 is logged in and the core forward builder
(bridge/apis/forward.ts) already defaults a zero sender to the bot's own uin.
Upstream frameworks (AstrBot, etc.) don't manage QQ uins and legitimately send
user_id "0"; NapCat and LLBot accept it. parseForwardNodes now falls back to
ref.selfId, keeping the throw only for the not-logged-in case (selfId 0).
Verified on-target (#203).
2026-07-05 23:00:13 +08:00
DrSmoothl 0e6e2ec51d chore(release): v1.11.3 2026-07-05 14:26:10 +08:00
DrSmoothl 8dde7ef8e2 fix(onebot): seed self nickname on synthetic self-sent events
get_msg / get_group_msg_history returned an empty sender.nickname for the bot's
own group file / video sends. Those publish via OIDB (0x6d9_4 / c2c finalize)
rather than PbSendMsg, so the QQ server never echoes them back — and the echo is
what normally overwrites the synthetic self-sent store's placeholder nickname.
For a plain text send the echo arrives and fills it, which is why only file/video
surfaced empty.

Seed the synthetic event's sender.nickname from the bot's own
identity.nickname (we are the sender, so we know it). A normal text send's echo
still overwrites it with the same value. Verified on-target: a self-sent group
video now shows the bot nickname in history.
2026-07-05 14:20:36 +08:00
DrSmoothl c236bc86ee feat(video): raise video-path size limit to 1.5 GiB (#145)
The 100 MB cap that re-routed larger videos to the file-upload pipeline
was a workaround for the width/height=0 → 已过期 bug (fixed in the prior
commit), not a real QQ size limit. Real-machine verified on tempserver:
88 MB / 149 MB / 299 MB / 497 MB videos all upload, render and play as
normal videos in a group. Raise MAX_VIDEO_SIZE to 1.5 GiB (matching the
file-upload ceiling); only videos above that still fall back to the file
path. Collapse the redundant MAX_VIDEO_SIZE_HARD (was also 1.5 GiB) into
the single constant and drop the now-dead 100 MB warn branches.

Note: the whole video is buffered in RAM for the Highway upload, so a
1.5 GiB send costs ~1.5 GiB+ of process memory — that, not a QQ limit,
is what bounds the ceiling.
2026-07-04 23:45:36 +08:00
DrSmoothl 770b224d58 feat(onebot): decode incoming 闪传 flash-file messages (#199/#200)
Older QQ clients (≤9.9.30) deliver a 闪传 (flash transfer) file as a richui
markdown commonElem (svc=45, busId=FlashTransfer); SnowLuma had no decoder for
it, so the message decoded to 0 elements and was reported empty. (Newer
clients ≥9.9.31 send a plain text+link message that already decodes fine.)

Decode svc=45 markdown → parse the FlashTransfer card JSON → emit a flash_file
element, surfaced as the segment the issue asked for:
  { type: "flash_file", data: { title, file_set_id, scene_type } }

- Field names (fileSetId / sceneType / title) confirmed against QQ NT's
  flash-transfer manager in wrapper.node.i64. The card is built by the
  sender's client so the exact JSON nesting is unknown — values are pulled by
  a bounded recursive search rather than a pinned path (handles either shape).
- Non-FlashTransfer markdown is ignored; missing fields default (never empty).
- Manifest: D/S = yes, P/W = by-design-no (receive-only; sending is the
  existing send_flash_msg action). Log preview shows [闪传文件:title].
- Tests: card decode (nested + alias + defaults + non-flash) and the
  element→segment mapping.

Note: not end-to-end verified on a live message — the svc=45 format only comes
from ≤9.9.30 senders and no such client was available (every test sender here
is ≥9.9.31 → text format). Logic + field names are unit-tested / IDA-confirmed.

typecheck 12 包; protocol 596 / onebot 529 green.
2026-07-04 19:02:47 +08:00
Airline233andCopilot Autofix powered by AI d3eb3bae14 feat(qzone,onebot): add set_qzone_ban action for qzone blacklist management (#193)
* feat(onebot,qzone): add set_qzone_ban action for qzone blacklist management

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:32:40 +08:00
DrSmoothl 6cb7f2c6d0 chore(release): v1.11.2 2026-07-04 13:39:00 +08:00
DrSmoothl 07c08ed74d feat(onebot): enrich group/member info fields (#197)
Round out the OneBot v11 group-profile fields the issue asked for.

Group (get_group_info / get_group_list, inherited by get_group_info_ex /
get_group_detail_info):
- group_create_time + group_memo come free from the 0xFE5_2 list
  (createdTime / announcement), verified live (createdTime=1729505343,
  announcement populated).
- group_level lives only in the 0x88D_0 detail; get_group_info fetches it
  (memoized 5min) so a joined group doesn't hit the detail every call.
  get_group_list leaves it 0 — a per-group detail fetch across the whole
  list would be far too costly.

Member (get_group_member_info / get_group_member_list):
- Add area / unfriendly / title_expire_time / card_changeable. QQ NT does
  not expose these, so they're fixed placeholders ('' / false / 0 / true),
  matching go-cqhttp / NapCat. sex/age stay unknown/0 (real values need a
  per-member profile fetch — out of scope here).

typecheck 12 packages; onebot 523 / core 472 green.
2026-07-04 13:04:32 +08:00
DrSmoothl b20ed12761 fix(onebot): approve group requests from the filtered inbox too (#197)
set_group_add_request could fail with "matching group request not found"
for a request SnowLuma had itself just reported. handleGroupAddRequest only
fetched the main inbox (0x10C8 subCommand 1); a spam-filtered join/invite
lives in the filtered inbox (subCommand 2) and was never searched, so its
flag couldn't be resolved to a sequence.

Fetch both inboxes and search the union. Each request keeps the `filtered`
flag its approval subCommand needs, so a filtered request is approved
through subCommand 2. A failing inbox fetch degrades to the other instead
of aborting. Regular MSF invite pushes don't carry the approval msgseq
(only the private-card path does, #125), so the fetch is the only source.

Regression tests: filtered-only request is found + approved via subCommand
2; "not found" only when neither inbox has it.
2026-07-04 12:48:06 +08:00
DrSmoothl c027161a64 fix(protocol): get_group_file_system_info returns real counts + space (#196)
file_count 恒为 0(且 used_space/limit_count/total_space 是占位/写死值)。根因:
0x6D8 view 命令分三个 subcommand(1=List, 2=Count, 3=Space),但 getCount 错发到
**subCommand 3(Space)** 且 busId=0,并从 pb<1>/pb<2> 读计数——服务端根本没返回
Count 块,于是恒 0。

真机 RE(bot 在测试群传/删文件核对):
- Count 走 **0x6D8_2 + busId=6**,响应 Count 块里 fileCount=**field 4**、maxCount(文件
  数上限,如 1500)=**field 6**(旧 pb<1>/pb<2> 全落空)。
- Space 走 **0x6D8_3**,响应 Space 块 totalSpace=**field 4**、usedSpace=**field 5**(64 位)。
- 实测:fileCount=4、limitCount=1500、usedSpace=20103729、totalSpace=10737418240。

修复:改 GetGroupFileCount 到 subCommand 2 + busId 6 + 正确 tag;新增 GetGroupFileSpace
(0x6D8_3);action 并发查两者,四个字段全返回**服务端实际值**(优于 NapCat 的写死占位)。
含 count/space 编解码回归测试。catalog regen。

typecheck 12 包;protocol 591 / onebot 521 / core 472 全绿。
2026-07-04 01:59:52 +08:00
DrSmoothl 95b5878456 feat(onebot): return new folder info from create_group_file_folder (#195)
create_group_file_folder 之前只回 data:null,拿不到新建文件夹的 folderId。现对齐
NapCat 返回结构,data 带回文件夹信息:
  { result: { retCode, retMsg }, groupItem: { folderInfo: { folderId, folderName,
    folderPath, createTime, modifyTime, createUin, modifyUin } } }

- 0x6D7_0 创建响应本就在 field 4 携带 folderInfo,SnowLuma 旧 proto 没解(只读
  retcode/retMsg/clientWording)。补上 OidbGroupFileFolderInfoResp 并让 create 的
  deserialize / bridge / action 一路把它带出来。
- 真机验证(bot 在测试群建夹→读回 folderId→删除清理):确认 folderInfo 存在,且
  **modifyUin 在 field 9 而非 Lagrange proto 标注的 field 7**——按真机 wire 修正。
- 新增两个解码回归测试(带 folderInfo / 缺 folderInfo 兜底)。catalog regen。

typecheck 12 包;protocol 587 / onebot 521 / core 472 全绿。
2026-07-04 01:25:14 +08:00
DrSmoothl 7744e24af2 fix(protocol): make set_group_search actually apply search flags (#191)
旧实现走 OIDB 0x89a_0 但 settings 发空字节 → 服务端返 ok 却 no-op;action 也只收
group_id,没有设置搜索方式的参数(报告属实)。

真机 RE 定稿(tempserver,bot 为管理员的实群逐字段扫描 + GET 读回):
- 修改命令 = 0x89a_0(全量抓包确认,与 MuteAll 同命令,靠 body 形状区分)。结构
  = {1:groupUin, 2: 只含被改字段的 GroupDetailInfo modify 子消息}。这是与 0x88D_0
  群详情 GET **不同的消息域**(mute/shutUpTime 在此=tag17,在 GET 里却是字符串),
  所以标签不能照搬 GET。
- 在 modify 子消息里逐 tag 试写、经 0x88D_0 GET 读回效果,定位:
  noFingerOpen(关键词/群指纹搜索)=tag 35,noCodeFingerOpen(群号搜索/总开关)=tag 36;
  再对照客户端「被搜索方式」三选下拉(私密/群号/群号+关键词)核实语义与 0/1 取值。
- proto-defs:settings 改为 Oidb0x89a_0SearchSettings 子消息,两 flag 用 pb_optional
  (显式存在)——否则 proto3 丢 0 值,导致关闭(=0)被静默忽略。undefined 仍省略。
- set-search/bridge/action:透传 no_finger_open / no_code_finger_open;只下发提供的
  字段(同 NapCat filter 机制、与 modify-group-ext-info 一致)。catalog regen。
- 编码测试锁定:0 值必发 tag 35/36、未提供则省略、命令为 0x89a_0。

typecheck 12 包干净;protocol 585 / onebot 521 / core 472 全绿。
2026-07-03 17:37:55 +08:00
Airline233 884db940df feat(qzone): add set_qzone_msg_right to change a feed's visibility (#186)
* feat(qzone): add set_qzone_msg_right to change a feed's visibility

Add updateQzoneMsgRight in the protocol layer, porting php-qzone's
updateRight: fetch the feed via emotion_cgi_msgdetail_v6, rebuild the
full publish-shaped payload (richval/pic_bo/con), then override
ugc_right/allow_uins and POST to emotion_cgi_update. Expose it through
QzoneApi.updateRight and the OneBot set_qzone_msg_right action, with the
same ugc_right/target_uins validation as send_qzone_msg. Covered by
protocol, core and onebot tests.

* fix(qzone): make qzoneParse more stable and fixed a test example
2026-07-02 22:38:05 +08:00
DrSmoothl 72aedbc612 chore(release): v1.11.1 2026-07-01 22:15:01 +08:00
DrSmoothl 3932958c39 fix(bridge,common,onebot): reject phantom hook UINs + never crash logging (#162)
注入的竞态窗口里,native hook 偶发上报 13 位垃圾 UIN(时间戳型,读到 QQ 尚未落定
的内存)。TS 层防御缺失,导致两个连锁问题:

1) 幽灵账号:isRealUin 只查「纯数字且 ≥5 位」、无上限,垃圾值通过后建会话并把
   config/onebot_<垃圾>.json 落盘 → OneBot 节点配置里出现随机幽灵账号(跨重启残留)。
2) 进程崩溃:logger 彩色路径 ' '.repeat(SLOT - tagLen),UIN tag(含方括号)超宽 →
   负数 → RangeError: Invalid count value → uncaughtException。

跨平台(Win DLL / Linux .so 同逻辑)+「更新 QQ 后消失」都印证是 native 读到那块内存
的偶发垃圾;根修在 SnowLumaNative,此处补齐 TS 防御:

- 抽出唯一的 @snowluma/common/uin::isRealUin = /^\d{5,10}$/(真实 QQ ≤ uint32 →
  10 位)。删除 bridge/core 两处重复副本(重复正是这次漏加上限的温床)。等价旧逻辑,
  仅新增拒 11+ 位。
- logger.ts 加 Math.max(0, …),日志永不崩进程(独立健壮化)。
- 收紧 WebUI UIN_REGEX /^\d{5,12}$/ → /^\d{5,10}$/ 保持一致。
- 启动时 cleanupInvalidPerUinConfigs() 清掉已落盘的幽灵 onebot_<uin>.json(只删
  UIN 不合法的,绝不碰合法账号/全局配置)。

测试:common +8(uin 6 / logger 2)、onebot +3(cleanup)。typecheck 12 包干净;
common 58 / onebot 519 / core 471 / bridge 73 全绿。对抗性 Agent 逐输入核验等价性、
三层 gate 堵死幽灵入口、cleanup 安全时序,无回归。
2026-07-01 20:37:02 +08:00
DrSmoothl a8f24b8f13 refactor(onebot): converge self-sent message tail into finalizeSend
send/forward 路径里「记录自发消息」的尾巴(hashMessageIdInt32 + cacheMessageMeta +
cacheSelfSentMessage)逐字复制 6 份(sendPrivate/sendGroup/两个 forward/forwardSingle
的 group+private 分支),每处独立手工配对 isGroup ↔ eventName ↔ hash 事件常量——
漏改一处会静默产生错 message_id / 错 event 归属。抽 finalizeSend(ref, isGroup,
targetId, receipt, elements),由 isGroup 单点派生 eventName + 常量。

6 处塌成 6 行调用;forwardSingle 的 group/private if/else 只剩 send API 本身不同。
receipt 用结构类型覆盖所有调用点(含 line 630 合成的 fallback)。

行为严格不变:onebot 516 全绿、typecheck 干净;对抗性 Agent 逐字对比 HEAD 确认
6 处等价、参数/返回正确。
2026-07-01 19:55:21 +08:00
DrSmoothl 85ca207279 refactor(onebot): extract openSqliteDb helper for the three stores
message/media/reaction store 构造器各自逐字复制同一段引导(mkdir 父目录 + 打开
DatabaseSync + PRAGMA journal_mode=WAL + PRAGMA synchronous=NORMAL),改一处 PRAGMA
要改三处。抽 openSqliteDb(dbPath) 收敛,schema init 与路径改写留在各调用方。

- message-store 的 .json→.db 改写保留(改写后同目录,mkdir 目标不变,等价)。
- 三文件删掉不再使用的 fs/path import。

行为严格不变:onebot 516 全绿、typecheck 干净;对抗性 Agent 逐字对比 HEAD 确认
三处引导等价、路径细节无边角、无回归。
2026-07-01 18:37:42 +08:00