Commit Graph
13 Commits
Author SHA1 Message Date
DrSmoothl adf6b6a3b4 test(protocol): isolate upload staging fixtures
Give cleanup tests a dedicated staging directory so they cannot remove files used by parallel upload tests.

Refs #520
2026-10-13 10:08:20 +08:00
DrSmoothl 7c0519f75f lint 2026-10-12 15:43:34 +08:00
DrSmoothl d86e0fc7b5 fix(protocol): preserve sent media for message history
Keep media sent by the bot available for later history and forwarding,
including messages without a server echo.

Fixes #462
2026-10-04 14:00:03 +08:00
DrSmoothl 0cc06b6b5e feat(onebot): send saved custom faces with sticker presentation
Resolve saved stickers by resource ID or MD5, support group and private destinations with optional replies, and preserve presentation in upload and message metadata.

Fixes #468
2026-09-26 16:19:45 +08:00
DrSmoothl 0a6c5b7d2a fix(protocol): attach voice waveforms on outgoing records
Private-chat clients only draw the progress track from message metadata.
Compute amplitudes when uploading a record so PC and phone show the bar.
2026-09-13 22:21:57 +08:00
DrSmoothl 31bd8ea77b test(protocol): cover highway streaming hash and upload staging 2026-08-14 10:57:17 +08:00
DrSmoothl 622c90d418 fix(protocol): transcode QQ AI voice recordings
Normalize the observed AI SILK container marker at the shared audio transcode boundary before invoking the bundled decoder. Preserve caller-owned bytes and leave ordinary SILK, AMR, and unrelated inputs unchanged.

Add unit and packaged-addon regression coverage for MP3 conversion, payload preservation, and input immutability.

Fixes #268
2026-07-21 23:29:28 +08:00
DrSmoothl 5fe6b31eb6 refactor(ci): generalize native smoke tests
Replace the feature-specific MP3 verification command with a workspace-wide native test entry point.

Keep native regression cases in package-owned test suites so future checks can expand without adding workflow steps.
2026-07-17 18:27:30 +08:00
DrSmoothl b3838d204f fix(protocol): produce valid MP3 records
Use the dedicated audio conversion path for record output so mono QQ voice records produce valid MP3 data.

Add release-target smoke coverage and preserve conversion and cleanup failures for diagnostics.

Fixes #239
2026-07-17 18:17:48 +08:00
DrSmoothl ade76d3d2f refactor(protocol): reuse tested Sha1Stream in video block-sha1 (drop dup)
video-upload.ts 有一份 ~110 行手写、**无测试**的流式 SHA1(Sha1StreamState),与
sha1-stream.ts 里已充分测试的 Sha1Stream 逐字节相同(transform 数学等价、中间 state
小端)。删掉该重复,computeVideoSha1Blocks 改用 Sha1Stream 做 1MB 块的中间 state、
Node crypto 做整文件 final(与受测的 computeSha1StateV 同一套路;已实测旧 final() ===
Node crypto 整文件 sha1)。

- 新增 video-sha1-blocks.test.ts(4 例):golden 值从**旧实现**抓取,锁定重写 byte-
  identical(两个 1MB 中间 state + overall),并给这个原本无测试的函数永久覆盖。
- 删掉的 tail-update 只影响旧自实现 final,不动任何中间块(tail<64B 不触发 1MB 边界)。

protocol 565 全绿、typecheck 干净;对抗性 Agent 独立复现旧实现确认 golden 逐字节
相同、transform 数学等价无精度问题、无残留引用。
2026-07-01 20:14:55 +08:00
DrSmoothl 90456def2e feat(onebot): get_record out_format — server-side voice transcode (#165)
#165: get_record gains an optional `out_format` (mp3/amr/wma/m4a/spx/ogg/
wav/flac, mirroring NapCat). When set, the QQ voice (SILK/AMR) is
transcoded server-side and the result is attached as `data.base64`; the
original info (file/url/file_size) is preserved. Absent ⇒ behaviour
unchanged. This spares ASR consumers from shipping a SILK decoder.

- protocol: convertAudioBytes(bytes, format, deps?) drives the bundled
  SILK-capable ffmpeg addon's convertFile through temp files (always
  cleaned up), with a format allowlist and an output-size cap. Pure/
  injectable, unit-tested with a fake addon (6 cases).
- core: ExtrasApi.convertRecord(source, format) = loadBinarySource (64 MiB
  cap) → convertAudioBytes.
- onebot: get_record wires out_format; transcode failure (incl. an addon
  lacking SILK decode) degrades to ACTION_FAILED, never crashes.

Adversarially reviewed: no path traversal (format allowlisted), no SSRF
(source is the cached CDN url, not user input), cleanup covers every path,
graceful degradation confirmed. Catalog regenerated. Real-machine e2e
(does the shipped .node actually decode SILK?) is Phase 2.
2026-06-30 19:25:40 +08:00
Qiao 127f2cd2d6 feat(flash-transfer): QQ flash transfer (fileset) upload, share, download (#113)
* feat(proto-defs): add flash-transfer OIDB proto types

QQ 闪传(FlashTransfer / fileset)的 protobuf 类型定义:
- FlashFileEntry(0x93d2/0x93d3 文件条目)
- 0x93cf 申请 fileSet / 0x93d0 commit / 0x93db 完成 / 0x93d1 设状态
- 0x93d3 文件集详情 / 0x93d4 下载 URL(含主文件 fileId + 缩略图)
- 0x12a9 sub=100/103/200 prepare/apply/get-download
- sliceupload body + Sha1StateV
- 0x9407 删除 / 0x9427 重命名 / 0x93d7 发送
注册到 oidb-actions/index.ts,package.json 加 exports。

* feat(protocol): add flash-transfer OIDB services + sha1-stream

闪传协议层(OIDB namespace,复用 invokeOidb/OidbCallSpec):
- apply-fileset / commit-file / complete-fileset / set-status(上传流程)
- prepare-upload (0x12a9_100) / apply-upload (0x12a9_103)(sliceupload 凭证)
- get-fileset-detail (0x93d3) / list-filesets (0x93d2) / get-download-url (0x93d4)
- get-flash-download (0x12a9_200,主文件下载直链)
- delete-file (0x9407) / rename-file (0x9427,sub=0+uinForm) / send-flash (0x93d7)
- sha1-stream:SHA1 中间 state 计算器(sliceupload f107.f6 需要,Node crypto 不支持)
- byte-oracle 单测(apply/prepare/get-download-url/send-flash)+ sha1-stream 单测
package.json 加 exports。

* feat(core): add FlashTransferApi facade

FlashTransferApi 编排闪传全流程,复用 OIDB 基础设施(invokeOidb)+
computeHashes/loadBinarySource/fetch:
- createFlashTask:统一走 sub=100/103+sliceupload(含缩略图),不走小文件 PUT
  (PUT 不上报主文件 sha1,fileset 卡在未完成;QQ 客户端小文件也走 sliceupload)
- buildFileId:主文件 appid=14901(进 f14.f1),缩略图 png=14903/jpg=14902
- uploadThumbnail:随机纯色 PNG 占位(手写编码,不引入图像库),关联主文件下载入口
- getFilesetInfo/getFlashFileList/getShareLink/getFlashFileUrl/downloadFileset
- sendFlashMsg(私聊 uid / 群聊 groupId)/ getFilesetIdByCode(网页解析)
- deleteFlashFile / renameFlashFile / listFilesets
注册到 ApiHub(flashTransfer 字段)+ export。

* feat(onebot): add flash-transfer actions

OneBot action(extended.ts)10 个:
create_flash_task / get_fileset_info / get_flash_file_list /
get_flash_file_url / get_share_link / download_fileset /
send_flash_msg(私聊 user_id / 群聊 group_id)/ get_fileset_id /
rename_flash_file / delete_flash_file
MCP catalog(生成文件)同步新增 action 元数据。
extended-actions 接线单测覆盖。

* fix(onebot): align flash file list field name with NapCat

flashFileInfoToJson 的文件大小字段从 file_size 改为 size,对齐 NapCat
get_flash_file_list 响应(NapCat 用 size)。get_fileset_info 的 file_list
复用同一映射,一并生效。便于 NapCat 客户端 drop-in 迁移。

* feat(proto-defs): support multi-file commit in flash-transfer

0x93d0 commit 的 f4 改为 repeated,一个请求携带 fileset 内全部文件条目。
FlashCommitFileInfo 的 f3/f5/f6/f7/f10/f12 用 pb_optional,强制 0 值上 wire
(服务端多文件时严格校验字段存在,省略 0 会导致后续文件不计入 fileset)。

* feat(protocol): parameterize flash-transfer upload for multi-file

CommitFile.Params 改为 entries 数组,serialize 输出 repeated f4,每条 f6=文件序号。
PrepareUpload/ApplyUpload 加 fileIndex + formatCode 参数:filesetWrap.f4 用 fileIndex
(与 commit f6 对齐),f7 用 formatCode(mp4=2 非 26),f9=1。

* feat(core): multi-file flash-transfer upload

createFlashTask 支持多文件共用一个 fileset。关键改动:
- ApplyFileset 多文件时 fileName 用「<首文件名>等N个文件」、fileSize 用总和,
  服务端据此判定 fileset 为多文件,commit 的后续 entry 才被计入
- 一次性 CommitFile 提交所有文件元数据(f4 repeated)
- 两阶段上传:先全部 prepare+apply 注册 fileId,再全部 sliceupload 落盘
- 拆分原 uploadLargeFile 为 prepareAndApply + sliceuploadFile
- fileTypeCode mp4 formatCode 26→2(与 commit f7/filesetWrap.f7 一致)

* feat(onebot): multi-file create_flash_task + list_filesets action

create_flash_task 的 files 参数支持 string 或 string[],多文件共用一个 fileset。
新增 list_filesets action(facade.listFilesets 之前未暴露)。MCP catalog 同步更新
files schema(anyOf string/array)与 list_filesets 条目。

* fix(flash-transfer): multi-file download by fileIndex

0x93d4 响应 f1.f3 是 repeated fileInfo(多文件 fileset 每个文件一条,f6=序号、
f14=主文件 fileId),之前定义成单个导致多文件只能拿到第一个文件。
FlashDownloadEntry.fileInfo 改 pb_repeated,GetDownloadUrl 返回所有文件元信息,
download_fileset 按 fileIndex 选中目标文件走 0x12a9 sub=200 拿直链。

* test(protocol): inline byte-oracle hex fixtures

两个 flash-transfer byte-oracle 测试原先用 readFileSync 读仓库外的
.claude/reverse/scripts/proto/*.hex(本地逆向笔记目录),CI 上不存在导致失败。
把 hex 内容内联成常量,测试自包含。

* fix(flash-transfer): address review feedback

1. get_flash_file_url 接 file_index 参数并在 action 透传(之前漏接,多文件永远返回首文件)
2. getFileDownload 越界 fileIndex 不再静默回退首文件,找不到返回 null
3. postSliceupload status 缺失不再当成功,非 success 一律抛错
4. computeSha1StateV 改 O(n) 增量 update(之前每片 reset 重算整个前缀,O(n²))
5. get_flash_file_url 找不到文件抛错(之前返回空串当成功)
6. create_flash_task files 数组拒绝空字符串
2026-06-21 14:24:51 +08:00
DrSmoothl 9d1e39f25f fix(highway): VP8X WebP canvas dims are little-endian, not big-endian (#112)
detectImageFormat read the VP8X (extended WebP) canvas width/height with a
big-endian readBE24, but the WebP spec stores them as 24-bit LITTLE-endian
(Minus-One). A 1920×1080 image was reported as 8324865×3605505, so the
fileInfo sent to highway was garbage and QQ cropped the thumbnail. VP8
(readLE16) and VP8L (readLE32) were already correct.

- Replace readBE24 with readLE24 in the VP8X branch; readBE24 had no other
  caller, so it's deleted (no dead code left).
- Add the previously-absent detectImageFormat regression suite (12 cases):
  PNG/GIF/BMP/VP8/VP8L/VP8X/JPEG + JPEG DHT-skip + truncated/unknown
  fallbacks. The VP8X cases pin little-endian byte order, including the
  exact bytes from the issue and an asymmetric 256×1 case that a BE misread
  would blow up to 16711681.

Verified TDD red→green: the 3 VP8X cases failed before the one-helper swap,
all 12 pass after.
2026-06-20 20:19:10 +08:00