363 Commits
Author SHA1 Message Date
手瓜一十雪 3870052fb4 Simplify Hub channel management
Refactor Hub to treat each UIN as a single active Channel: channelsByUin_ now maps UIN -> Channel (no Set of IDs), primary selection logic and primaryByUin_/pickPrimary were removed, and addChannel replaces any existing channel for a UIN (disposing the previous one). Updated add/remove flows to update channelsById_ and channelsByUin_ accordingly and to bring cores online/offline in the simplified model.

Also: removed extensive JSDoc/comment blocks from channel/adapter/channel-ctx/core/hub files to slim the public API surface, reordered/cleaned exports in packages/channel/src/index.ts, added a new hub-events.ts placeholder, and cleaned up a test helper comment. Disposal of replaced channels is wrapped with a try/catch and logged at debug level.
2026-05-27 18:07:03 +08:00
手瓜一十雪 b019b00e82 Set protobuf plugin root for OneBot tests
Pass the monorepo root to @snowluma/proton's Vite plugin so protobuf encode/decode call sites in transitive workspace packages are transformed during testing. Adds path/fileURLToPath imports and __dirname, and calls protobufVitePlugin({ root: path.resolve(__dirname, '../..') }) to mirror @snowluma/core's setup and avoid "was not transformed" errors in OneBot tests.
2026-05-27 14:21:51 +08:00
手瓜一十雪 50aa9b3595 Allow vitest to pass with no tests
Update packages/channel/package.json test script to run `vitest run --passWithNoTests` so CI or local runs won't fail when the package has no tests. No other behavior changes.
2026-05-27 14:15:44 +08:00
手瓜一十雪 af8d5ab06b Split channel packages and rename Account→Core
Reorganize package layout and rename core/account types: - Move channel hook & socket sources/tests into packages/channel-hook and packages/channel-socket (many files renamed). - Remove ChannelManager from packages/channel and drop its export from package.json (multi-account host folded into Hub). - Update tests to use HookSink instead of BridgeManagerSink. - In @snowluma/core: move account/apis/* up to src/apis, rename account-context→core-context, account-interface→core-ctx, account→core, and add hub.ts; delete AccountManager. - Update constructors and type usages to accept CoreContext/CoreCtx and adapt identifiers accordingly. This is a refactor to clarify responsibilities (split transport implementations, fold multi-account manager into Hub, and rebrand the account layer as Core).
2026-05-27 14:11:45 +08:00
手瓜一十雪 adf0744f75 Rename bridge → channel and reorganize packages
Move/rename the transport/bridge code into a new @snowluma/channel layout and extract hook implementations into channel-specific packages. Many bridge sources were deleted and equivalent channel files added (adapter/ctx/interface/channel/hook/socket pieces moved/renamed), along with tests and configs relocated under packages/channel. RoadMap.md replaced with a detailed v3 architecture plan describing Channel/ChannelCtx/Core/CoreCtx/Hub boundaries and migration steps. Also apply related core package updates (account/core reshaping and package.json adjustments) to match the new architecture.
2026-05-27 13:39:17 +08:00
手瓜一十雪 652bafd75a Extract bridge transport into @snowluma/bridge
Move transport-layer bridge code out of @snowluma/core into a dedicated @snowluma/bridge package and introduce a clear account layer in core. Adds Bridge, BridgeInterface, InjectBridge, ProtocolBridge (stub), adapters, and manager files under packages/bridge and updates package.json exports. In @snowluma/core the former bridge surface is replaced by Account, AccountContext, AccountInterface and AccountManager; apis were moved/renamed to account/apis and now consume AccountContext. Adjustments include packet handling plumbing (use deliverPacket / setPacketHandler), updated sendRawPacket wiring, uploaded-file cache and lifecycle changes (InjectBridge.dispose, ProtocolBridge returns a structured not-implemented result). Tests and imports were updated accordingly.
2026-05-27 12:27:13 +08:00
手瓜一十雪 5440cbc37e Refactor bridge system: adapters & transports
Introduce a transport-adapter layer and refactor the bridge abstractions to be transport-agnostic. Added BridgeAdapter and BridgeAdapterHost (packages/core/src/bridge/adapter.ts) plus concrete InjectBridge/InjectBridgeAdapter and ProtocolBridge/ProtocolBridgeAdapter implementations (new files). Made Bridge an abstract base exposing kind/id and abstract sendRawPacket, moved transport-specific PID/sender logic into InjectBridge. Reworked BridgeManager to register/start adapters, manage bridges by id/uin, pick a primary per-UIN (inject preferred over protocol), and provide a host API for adapters to add/remove bridges. Updated index bootstrapping to register adapters and wire the WebUI to the inject adapter's HookManager. Adjusted tests and OneBot code to remove PID tracking from OneBotInstance/OneBotManager and to instantiate InjectBridge in tests. Also exported new entry points in package.json for adapter/bridge modules. This decouples transports from manager logic and adds a stubbed protocol runtime for future pure-protocol support.
2026-05-27 11:49:43 +08:00
手瓜一十雪 a434da9e85 Skip resolveImports for files without subclasses
Add a cheap AST probe in file-pipeline to detect if a file contains extending classes (using fileHasExtendingClass) and return early to avoid the expensive resolveImports recursive descent when not needed.

Update subclass-wrapper tests to import beforeAll and cache the plugin transform output and its stripped JS once in a beforeAll hook, replacing repeated transform calls with reused transformedOutput/strippedJs to reduce test runtime.
2026-05-26 22:02:31 +08:00
手瓜一十雪 2c565fbd8d Switch from @snowluma/sqlite to node:sqlite (sync)
Remove the in-tree @snowluma/sqlite wrapper and vendored better-sqlite3 prebuilts; migrate consumers to node:sqlite's synchronous API. Replaced imports and types (Database/Statement -> DatabaseSync/StatementSync), updated PRAGMA calls to use exec, and adjusted code in onebot (media/message/reaction stores) and protocol (identity service). Vite config no longer bundles sqlite native binaries and references to the sqlite workspace were removed. Also delete the packages/sqlite package and its native prebuild artifacts, and add a couple of unrelated AST improvements in proton: use a module-level shared wrapper cache and add an inheritance cache to avoid repeated scans. Minor import reordering/cleanup applied where needed.
2026-05-26 21:59:26 +08:00
手瓜一十雪 0d30ee628e Switch subclass-wrapper to AST-only pipeline
Refactor subclass-wrapper resolution to an AST-only pipeline and remove the TypeChecker-based program cache.

- Move subclass-wrapper logic from typecheck/ to ast/: add ast/file-pipeline.ts and ast/subclass-wrapper.ts, and update codegen/imports accordingly.
- Implement AST-only cross-file extends resolution using import-resolver's fileCache (findImportSourcePath) and a WrapperLookupCache; subclass methods must now have explicit type annotations (no TypeChecker inference).
- Remove TypeChecker-dependent program-cache and typecheck/file-pipeline; plugin no longer preloads a ts.Program or accepts tsconfig option.
- Simplify plugin cache startup and HMR handling in src/index.ts to use the AST pipeline and transform cache only.
- Update tests to use the new AST pipeline entrypoints and adjust expectations.

This eliminates the dependency on a baked ts.Program for wrapper resolution, simplifies startup, and relies on the existing import-resolver for cross-file traversal. Partial wrapper resolutions are still all-or-nothing; explicit annotations are required for AST-only resolution.
2026-05-26 21:48:12 +08:00
手瓜一十雪 0f7aa5a600 Refactor import & wrapper typecheck APIs
Consolidate and simplify import extraction and wrapper/typecheck internals. Introduce collectImportClauses that drives both type+value and value-only import extraction with per-file caching; reintroduce extractImports/extractValueImports as thin wrappers. Clean up BARE_SPECIFIER_COMPILER_OPTIONS comment. Replace the containsTypeParameter/firstTypeParameterIndex pair with a single findTypeParameterIndex helper. Remove collectExportedWrappers and call collectWrappers directly from parseFileForDefinitions; ensure imported wrapper collection skips type-only imports to avoid eager parsing.

Streamline static-wrapper representations: ClassWrapperInfo is now a Map<string, WrapperMethodInfo>, and related detection helpers updated and documented. Simplify subclass-wrapper/typechecker APIs: drop wrapperOwner and SourceFile parameters where unused, adjust function signatures (collectInheritedWrappers, resolveSubclassWrappers, resolveWrapperCodecCalls, extractSubclassMemberTypeNode) to operate with fewer arguments and return plain WrapperMethodInfo lists. Update file-pipeline to match reordered args.

Refactor plugin context creation: add buildContext helper, createPluginContext/createStandaloneContext now delegate to it; remove programPrepared flag and adjust prepareProgramForTsconfig invocation site. Update tests to reflect Map API changes and revised function signatures.

Overall this is an internal refactor to reduce duplication, avoid eager work for type-only imports, remove unused params, and clarify interfaces without changing behavior.
2026-05-26 21:24:37 +08:00
手瓜一十雪 1b07005d91 Unify protobuf helpers and tidy comments
Export and centralize small utility helpers and remove duplicate implementations: identityImportedTypeName was exported from utils, and isVarint64/isFixed64BigInt were added to types and imported into encoder/decoder. Minor import reorders and formatting cleanups across collector, monomorphizer, import-resolver, static-wrapper, subclass-override and file-pipeline. Comments and JSDoc were tightened for clarity; behavior remains the same but common logic is now deduplicated and easier to maintain.
2026-05-26 20:53:32 +08:00
手瓜一十雪 aaed36aa19 Remove verbose header and tidy release script
Strip the large one-shot release driver comment block at the top of tools/release.mjs and apply minor formatting cleanups (compact ok() logging line and adjust spacing for the `ahead` variable). No functional changes intended — just cleanup of comments and whitespace.
2026-05-26 18:29:32 +08:00
手瓜一十雪 c3a8a9b6b6 Revert "Add transform + program caching to plugin"
This reverts commit 868f4a5798.
2026-05-25 12:02:22 +08:00
手瓜一十雪 868f4a5798 Add transform + program caching to plugin
Introduce disk caching and startup program preparation for the Proton Vite plugin. Add ProtobufVitePluginOptions (root, tsconfig, cache) and configResolved hook to prebuild/prepare a TypeScript Program (program cache) and create a transform cache per-file (transform cache) keyed by source/config hashes and dependency mtimes. Track dependencyFiles in import-resolver so transform cache can validate mtimes. Update program-cache to support incremental tsbuildinfo and expose prepareProgramForTsconfig. Add cache read/write, hashing, path normalization and scope checks in the plugin; update tests and README to cover scope and cache behavior. Also add ts-morph dependency and update lockfile.
2026-05-25 11:52:46 +08:00
手瓜一十雪 93850ec11c Add text-edit utility and import resolution caches
Introduce a TextEdit type and applyTextEdits utility for safe, reverse-ordered string edits and refactor replacer to produce and consume edit lists (collectReplacementEdits) instead of performing manual slicing. Propagate ts.SourceFile through ParsedFileEntry/ImportedDefinitions and use cached extraction of imports/exports to avoid repeated AST traversal when resolving imports; analyzer now prefers an imported sourceFile when available. Wire text edits into the plugin pipeline (compose replacement + override edits) and remove the previous applyOverrideInsertions helper. Add a tsconfig lookup cache in program-cache to speed up upward tsconfig searches and invalidate it when the program cache is cleared. README updated to list the new files.
2026-05-25 11:42:03 +08:00
手瓜一十雪 40c51bcd02 Extract wrapper binding logic to wrapper-binding.ts
Move instantiateWrapperTypePattern and the WrapperBinding type into a new wrapper-binding.ts module and register it for reuse. Update analyzer.ts and import-resolver.ts to import the function/type from the new module and simplify call-site/type-root collection logic (use entrySourceFile and a cached callable name). Also update README: add ast-helpers.ts and wrapper-binding.ts to the file list and bump test count in the example.
2026-05-25 11:34:37 +08:00
手瓜一十雪 bed48bbe46 Skip subclass-wrapper pipeline when no subclasses
Avoid the expensive TypeChecker/program startup for files that don't declare extending classes by performing a cheap top-level scan first. index.ts now imports fileHasExtendingClass and a SubclassWrapperPipelineResult type and conditionally calls runSubclassWrapperPipeline only when a candidate is found, otherwise returning an empty result. Also updated comments to explain the optimization. subclass-wrapper.ts now imports isStaticMethod from ast-helpers.js (small import cleanup/usage update).
2026-05-25 11:30:41 +08:00
手瓜一十雪 34cc6be8df Add AST helpers, cache resolver, translate README
Add shared AST predicates in packages/proton/src/ast/ast-helpers.ts to centralize small structural checks used by the analyzer, callsite matcher, wrapper detector, and subclass resolver. Introduce a WeakMap cache in packages/proton/src/ast/utils.ts so createImportedTypeNameResolver is memoized per SourceFile to avoid repeated import-scans during transform() work. Update packages/proton/README.md with a Chinese translation and various clarifications/documentation tweaks.
2026-05-25 11:28:05 +08:00
手瓜一十雪 c55042aff4 Splice subclass override code into source
Group rendered subclass overrides per class and emit them as position-tagged insertions instead of a single trailing string. Add SubclassOverrideInsertion and applyOverrideInsertions to splice overrides immediately after each subclass declaration (sorted in reverse to keep offsets valid). Update runSubclassWrapperPipeline to collect per-subclass rendered parts and return insertions + extra root names, and update the plugin transform to apply these insertions before producing final code. Update tests to expect insertions and verify placement and runtime behavior. This ensures user code sees overrides right after the class body so subsequent top-level calls dispatch to the override rather than an inherited abstract implementation.
2026-05-25 11:20:44 +08:00
手瓜一十雪 3726913c1f Support per-subclass static wrappers; drop runtime map
Introduce per-subclass static-method wrapper support: detect wrapper-pattern methods in the AST, resolve concrete types via a TypeChecker, render per-subclass overrides, and append them into transformed output. Adds static-wrapper detector, subclass-override codegen, a typecheck file pipeline, a lazy ts.Program cache, and the subclass-wrapper resolver. Integrates the pipeline into the Vite plugin (including program cache invalidation on HMR) and threads extra root type names into registry selection so generated overrides get required codecs. Remove the runtime-map fallback feature and related types/logic (runtime-map module, dynamic runtime generation, and public API surface), simplifying runtime stubs and protobuf.d.ts. Update README and analyzer to document and support the new flow; add tests and fixtures for subclass-wrapper behavior.
2026-05-25 11:17:31 +08:00
DrSmoothl 5924346823 chore(release): v1.9.2 2026-05-24 18:01:50 +08:00
手瓜一十雪 456086599c Normalize imports, typings and formatting
Reorder and normalize imports, add missing type imports, and fix whitespace/formatting across multiple files. Notable updates: eslint.config.ts import ordering; core bridge APIs (group-album, misc, web) now import Json types; tests/highway pipeline import ordering and added type imports; protocol changes (element-builder, highway client/pipeline) reorganized imports and added proto type imports; many oidb service modules moved protobuf encode/decode imports for consistency; web group modules (album, essence, honor, notice, request-util) reordered imports, adjusted typings/indentation, and small whitespace fixes. These edits are stylistic and typing improvements and should not change runtime behavior.
2026-05-24 13:39:35 +08:00
手瓜一十雪 80ee12ecc2 Enforce no-explicit-any; allow in tests
Enable @typescript-eslint/no-explicit-any as an error in the ESLint config to tighten typing across the codebase, and add an override to disable this rule for test files. Also apply non-functional cleanup in packages/proton/src/runtime.ts: reorder/imports and adjust formatting/indentation and string alignment for readability — no behavioral changes.
2026-05-24 13:38:13 +08:00
手瓜一十雪 acc5084647 Refactor group-album and remove eslint ignore
Remove an unused Bridge import, the asBridge helper and a stale comment from group-album, and relocate the GroupAlbumMediaResult and GroupAlbumCommentResult interfaces to the end of the file. In the sqlite package, remove the inline eslint-disable-next-line for @typescript-eslint/no-explicit-any near the BetterSqlite3 constructor. These edits clean up dead/stale code and re-enable the lint rule; there are no behavioral changes.
2026-05-24 13:16:36 +08:00
手瓜一十雪 afbec8ed44 Cleanup imports and formatting in bridge
Reorder and tidy imports, re-export types, and apply minor formatting fixes across the bridge package. Changes include:

- Reordered imports and grouped related imports in hook-manager, hook-session, pipe-watcher, qq-hook-client, qq-port-probe and others.
- Moved several type re-exports (HookProcessBaseInfo, QqPortLoginInfo, HookProcessInfo/Status) in hook-manager and consolidated exports in package entry index.ts.
- Minor whitespace/style tweaks: constructor spacing, for-loop formatting, empty catch handlers, and small refactors that do not change runtime behavior.

These are non-functional housekeeping changes to improve consistency and readability.
2026-05-24 13:14:36 +08:00
手瓜一十雪 a49e507e15 Refactor Bridge APIs, slim Bridge and reorder imports
Move and standardize API implementations under bridge/apis, slim down the Bridge class (remove inline action implementations, caches and large comment blocks) and relocate related types/exports. Clean up and reorder imports across many bridge/api modules, normalize constructors, add logger imports where needed, and adjust WebUI routing/import ordering. These changes centralize API logic, reduce Bridge surface area, and tidy module boundaries and imports.
2026-05-24 13:13:23 +08:00
手瓜一十雪 a52a9724cb onebot: clean up comments, imports and whitespace
Remove extraneous comments, redundant explanatory notes and extra blank lines across onebot packages; reorder and normalize import lists in group-admin and group-album; remove unused comment blocks in event-converter. These are non-functional cleanup changes in packages/onebot/src/actions/extended.ts, packages/onebot/src/actions/group-admin.ts, packages/onebot/src/actions/group-album.ts and packages/onebot/src/event-converter/index.ts to improve readability and maintain consistent style.
2026-05-24 13:08:24 +08:00
手瓜一十雪 245405d294 Refactor comments and small type fixes in extended.ts
Add JsonObject import and use it for handleSendPacket param typing; replace deprecated hex.substr with hex.slice; convert and consolidate many English docstrings/comments into Chinese for clarity; remove some JSDoc noise and minor formatting tweaks; improve explanatory comments around streaming download, base64 size checks, safe path resolution and async file writes. These are primarily documentation and small type/cleanup changes with no functional behavior changes.
2026-05-24 12:54:21 +08:00
DrSmoothl f53df5e313 refactor(packages): extract @snowluma/bridge (hook + QQ lifecycle)
Split the QQ NT client lifecycle / hook injection layer out of
@snowluma/core into its own self-contained package. The new bridge
package owns:
  - native hook addon loader (injector)
  - HookManager + per-PID HookSession orchestration
  - PipeWatcher for QQ process discovery
  - QqHookClient pipe protocol
  - qq-port-probe for early UIN sniffing

The hook layer used to type its main collaborator as
`BridgeManager` directly. To keep the new package free of any
dependency on @snowluma/core, the cross-package coupling is now
expressed via a 3-method `BridgeManagerSink` interface that lives
in the bridge package. BridgeManager (in core) satisfies the
interface structurally with no source changes; the top-level
app/webui still inject it the same way.

Pure file relocation otherwise: git mv preserves history at 100%
on every file, the only logic edit is the interface introduction
in hook-manager.ts and an updated `nativeSearchDirs()` offset
(one fewer `..` since hook now lives at packages/bridge/src
instead of packages/core/src/hook).
2026-05-23 23:40:57 +08:00
DrSmoothl c16064d7e3 refactor(packages): rename @snowluma/bridge → @snowluma/protocol
The package previously named "bridge" houses the protocol-byte layer
(OIDB cmds, msg-push decoders, highway uploaders, element-builder,
event bus). The name "bridge" suggested transport/lifecycle, which it
isn't — that role lives in core/hook and core/bridge/manager. Rename
to match what's actually inside.

This is a pure rename: zero logic changes, all 73 import sites updated
mechanically and TS-verified. core/onebot dependencies key updated.
ESLint override path adjusted for the moved oidb-services directory.

Rename detection threshold: git log --find-renames=20% recommended
when following history across this commit — content-replacement
(@snowluma/bridge → @snowluma/protocol) dropped per-file similarity
below git's default 50% on import-heavy files.
2026-05-23 23:31:03 +08:00
DrSmoothl 98296fcec5 fix(send-like): update serialization to resolve user UID and adjust payload structure 2026-05-23 22:50:40 +08:00
DrSmoothl d8c72c2ebc fix(events): surface verify-text on group_invite ("你们好" → comment)
User report:
  SnowLuma's group_invite event renders to OneBot with empty
  `comment`, so the bot's approval-prompt template shows nickname
  + uin + flag but loses the requester's verify message.

  NapCat's same flow shows the verify text:
    【进群申请】批准/驳回:
    昵称:KitaIkuyo
    QQ:1957003260
    flag:1779543612823906
    等级:7
    你们好               ← only NapCat shows this line

Root cause:
  The OIDB push (GroupRequestJoinNotice subType=84) only carries
  `(groupUin, targetUid)` and a few opaque numeric fields — no
  string for the verify text. NapCat doesn't use the push directly;
  they poll `nodeIKernelGroupService.getGroupNotifies` (= our OIDB
  0x10C0 pending-request queue at `FetchGroupRequests`) which
  returns each pending row with `postscript` (the verify text) and
  `seq` (server sequence id). Reference:
    dev/NapCatQQ/packages/napcat-onebot/index.ts:483-498
    (`flag = notify.seq`, `comment = notify.postscript`).

  Our pipeline didn't enrich. The decoder set `message: ''` and
  emitted, so OneBot's `comment` was empty.

Fix:
  - Extend `PacketPipelineDeps` with a `resolveGroupJoinRequest(
    groupId, uid, subType)` hook. Bridge.ts wires it to
    `fetchGroupRequests` and matches on `(groupId, uid)` —
    targetUid for `subType='add'`, invitorUid for
    `subType='invite'`.
  - `dispatchAfterStrangerResolve` now fires the profile + request
    lookups in PARALLEL via `Promise.allSettled` so a flake on one
    path doesn't kill the other. The request lookup's `comment`
    lands on `event.message`; OneBot converts that to `comment`.

Decisions:
  - Kept the existing `add:<groupId>:<uid>` flag format (consumer
    bots already parse it). Switching to NapCat's sequence-form
    flag would break callers and is a separate breaking change.
  - Both lookups fail-open: if either rejects, the event still
    emits with whatever info we did get, so the bot at least sees
    the request and can render the bare nickname.

Regression tests:
  - `populates event.message from the pending-request queue` — pins
    the user-reported "你们好" case end-to-end through the pipeline.
  - `runs lookups in parallel` — profile resolves, request fails;
    event still ships with the resolved uin and an empty message.
  - `invite subtype matches the request on invitorUid` — explicit
    contract for the invite-forwarded-to-admin case where the
    pending row's REQUESTER is `invitorUid`, not `targetUid`.

345 bridge tests pass (was 342 + 3 new); typecheck + lint clean.
2026-05-23 22:00:14 +08:00
DrSmoothl 8631453940 fix(events): self-heal group_invite cache pollution from pre-fix builds
User report: after [a4909ec], log STILL shows
  群邀请 950929451 -> [bot group(950929451)]
  get_stranger_info params=user_id=950929451 → 未知昵称

But NapCat and a sibling SnowLuma bot in the same group BOTH decoded
the same push as `QQ:1957003260 nickname:KitaIkuyo`. So the wire is
correct — our bot is just looking it up wrong.

Root cause: **legacy cache pollution**. The pre-fix decoder fallback
stored `<requester_uid> → <groupUin>` in the identity DB whenever
`resolveUidToUin` missed (because the fallback `ctx.fromUin` on a
group-scoped push IS the group's own uin). After upgrading to
[a4909ec], `decodeGroupJoinRequest` calls `resolveUidToUin` which
hits the polluted in-memory map (re-populated from SQLite at boot)
and returns the groupUin instead of 0. The new `needsStrangerResolve`
guard only fired on `fromUin <= 0` — so the polluted value flowed
straight through and re-emitted the bug.

Two-part heal:

  1. `needsStrangerResolve` now ALSO fires when `fromUin === groupId`
     — the pollution signature. A real requester's uin can never
     equal the group's own uin, so this is a safe override that
     forces the async UID-form FetchUserProfile lookup, which then
     `rememberUidUin`s the correct mapping on success (eviction is
     already wired in identity-service.ts:468-471).

  2. `rememberEventIdentity` for group_invite now passes `uin: 0`
     when `event.fromUin === event.groupId`, so future writes don't
     re-pollute the cache (`rememberUidUin` short-circuits on
     normalized uin ≤ 0).

Cache self-heals on first event after upgrade — no manual DB nuke
needed.

Regression test pins the legacy-pollution path: feeds an event
where `fromUin === groupId` (matching the user's log), verifies the
resolver fires and patches fromUin to the correct value.
2026-05-23 18:40:01 +08:00
DrSmoothl a4909ec9e5 fix(events): resolve stranger nickname on group_invite (no more 未知昵称)
User report: when a stranger requests to join a group, the bot
receives `user_id=<groupId>` (instead of the requester's actual
uin) and `get_stranger_info` returns no nickname → bot displays
"未知昵称".

Two layered bugs:

  1. **Decoder fallback was wrong.** `decodeGroupJoinRequest` /
     `decodeGroupInvitation` / `decodeGroupInvite` all passed
     `ctx.fromUin` as the fallback uin when `resolveUidToUin` missed
     the local cache. But for group-scoped pushes `ctx.fromUin` is
     the GROUP's own uin — falling back to it produces the bogus
     `fromUin === groupId` collision the user saw. Fix: pass 0.

  2. **No async resolve for stranger UIDs.** The push only carries
     the requester's UID string. When the UID isn't in our cache
     (true for any stranger who isn't yet a group member), we have
     no UIN — and our UIN-form `FetchUserProfile` can't help. The
     pipeline never tried to look up the stranger by UID, so the
     event went out with `fromUin=0` (or, with the old fallback,
     the group's own uin).

Cross-checked against Lagrange.Core's flow:

  dev/Lagrange.Core/.../MessagingLogic.cs:215-224
    case GroupSysRequestJoinEvent join:
        var fetchUidEvent = FetchUserInfoEvent.Create(join.TargetUid);
        var results = await Collection.Business.SendEvent(fetchUidEvent);
        uint targetUin = results.Count == 0 ? 0
            : ((FetchUserInfoEvent)results[0]).UserInfo.Uin;
        var joinArgs = new GroupJoinRequestEvent(join.GroupUin, targetUin);

  dev/Lagrange.Core/.../OidbSvcTrpcTcp0xFE1_2.cs:9-26
    — UID-form variant of the same OIDB 0xFE1_2: field 1 is `uid:
      string`, envelope `reserved=0` (no uinForm flag).

  dev/Lagrange.Core/.../FetchUserInfoService.cs:66-78
    — picks UID vs UIN form based on which input is set.

Changes:

  - `proto-defs/oidb-actions/base.ts`: add `OidbUserInfoByUidRequest`
    (same shape as the UIN variant but field 1 is `uid: string`).
  - `bridge/oidb-services/contacts/fetch-user-profile-by-uid.ts`:
    new namespace at 0xFE1_2 with UID-form encoding, no `uinForm`.
  - `bridge/msg-push/decoders/group-join-request.ts`: 3 decoders'
    `resolveUidToUin` fallbacks change `ctx.fromUin` → 0.
  - `bridge/packet-pipeline.ts`: new `resolveStrangerProfile(uid)`
    dep + `needsStrangerResolve` check + async dispatch that fires
    the lookup before emitting the event. Fail open if the lookup
    rejects (event still ships with `fromUin=0` so the OneBot bot
    can fall back to the uid in the flag).
  - `core/bridge/apis/contacts.ts`: `fetchUserProfileByUid(uid)`
    facade method.
  - `core/bridge/bridge.ts`: wire `resolveStrangerProfile` into the
    pipeline deps so it routes through the new namespace.

Tests:
  - UID-form namespace: declares the right cmd/subcmd, packages uid
    as field 1, decodes nickname + uin so the pipeline patch works.
  - Pipeline stranger resolve: patches event.fromUin from the
    resolver response; fails open on null / throw; skips the wire
    call when fromUin is already populated or fromUid is empty.

All 655 tests pass (was 645 + 10 new); typecheck + lint clean.
2026-05-23 17:15:59 +08:00
DrSmoothl 4755a91cb3 fix(image): use Chinese summary defaults to match QQ ecosystem
Mobile QQ, Lagrange.Core, and NapCat all use the literal Chinese
bubble-preview strings `[图片]` / `[动画表情]`. SnowLuma was emitting
English `[image]` / `[sticker]` as fallback, so QQ clients showed
"[image]" in chat-list previews instead of the expected Chinese.

Cross-checked against:
  dev/NapCatQQ/.../packet/message/element.ts:367-371
  dev/Lagrange.Core/.../Message/Entity/ImageEntity.cs:186-192

Both branch the same way: subType=1 → `[动画表情]`, else → `[图片]`.

Changes:
  - `bridge/highway/image-upload.ts`: both code paths (fingerprint +
    load-from-source) now emit the Chinese defaults. Subtype branch
    flipped to `subType === 1 ? '[动画表情]' : '[图片]'` to match the
    upstream convention (our old check was `subType === 0` →
    `[image]`, which would label any non-zero subType — not just
    stickers — as `[sticker]`).
  - `bridge/msg-push/rich-body-decoder.ts`: all three decode paths
    (NotOnlineImage / CustomFace / commonElem image) now substitute
    the Chinese default when the server omits `textSummary`. Empty-
    string is also treated as missing (matches NapCat's `=== ''`).
  - Tests updated to assert the Chinese defaults; new test pins
    that a caller-supplied summary still overrides verdef.

The OneBot caller's `summary` field on the MessageElement still
wins when set, so downstream clients passing custom bubble text
(e.g. for accessibility) keep working.
2026-05-23 15:05:28 +08:00
DrSmoothl c8f259ef4a test(highway): pin multi-image sequential semantics + richer 921 diag
User reported `highway upload error_code=921` when sending two images
in one OneBot message — image 1 fast-uploaded (server already had the
md5), image 2 (58 bytes) triggered the actual PUT and failed.

Verified the claim from code inspection vs NapCatQQ + Lagrange.Core:

  - dev/NapCatQQ/.../highwayContext.ts:62-94 (checkAvailable + prepareUpload)
  - dev/NapCatQQ/.../highwayHttpUploader.ts:8-43 (HTTP block upload)
  - dev/NapCatQQ/.../highwayContext.ts:219-254 (uploadGroupImage)
  - dev/Lagrange.Core/.../HighwayContext.cs:98-180 (UploadSrcByStreamAsync)

Findings:

  1. The pipeline runs `runNtv2Upload` sequentially per image (await
     inside `buildSendElems`); there's no concurrency or shared state.
  2. Each call gets its own OIDB request, its own lazy session fetch,
     its own buildHighwayExtend output. No aliasing.
  3. The 921 comes from the highway PUT's response head (errorCode),
     not from anything client-emitted being malformed.

Verdict: not reproducible from code inspection — almost certainly
server-side (most likely Tencent rejecting the 58-byte image as a
malformed payload; 58B is too small to even be a valid 1×1 PNG).

Hardening:

  - Three regression tests pin the sequential two-image semantics so
    a future state-leak bug fails loudly:
      - extend bytes for image 2 must come from image 2's response,
        not image 1's,
      - each runNtv2Upload fetches its own non-null session,
      - each OIDB request carries its own fileInfo + a unique
        clientRandomId.
  - The `error_code=N` message now carries segRetCode, cmdId, chunk
    size, offset, and the first 8 bytes of the file md5 — without
    these the user couldn't tell apart "image rejected" vs "session
    expired" vs "rate-limited" on future reports.

If the user reports the same 921 again, the new diagnostic message
should show enough server-side context to triage server-vs-client.
2026-05-23 13:06:09 +08:00
DrSmoothl cd9403f27a fix(forward): align resid+uniseq for nested forwards
Multi-layer 合并转发 was uploading correctly but the outer's
LongMsgResult piggyback was *unreachable* by the receiver because
two things were missing:

  1. The forward preview element was emitted as the old XML
     `richMsg serviceID=35 m_resid=…` shape, which carries the
     inner resId but has NO uniseq field.
  2. The piggyback `actionCommand: <uuid>` slot generated a fresh
     UUID that was never reflected into the preview either.

Result: QQ-NT receivers saw the inner preview's resid, scanned the
outer's actions[] for a matching uniseq, found none, and either
fell back to a fresh server fetch per layer or dropped the layer
entirely.

Cross-checked against:
  - dev/Lagrange.Core/Lagrange.Core/Message/Entity/MultiMsgEntity.cs:43-115
  - dev/NapCatQQ/packages/napcat-core/helper/forward-msg-builder.ts:52-122
  - dev/NapCatQQ/packages/napcat-onebot/action/msg/SendMsg.ts:208-347

Both upstream impls use the modern `com.tencent.multimsg` LightApp
JSON whose `meta.detail.uniseq` is the round-trip key that links
the inner preview to the matching `actionCommand` in the outer's
LongMsgResult — no separate fetch needed.

Changes:
  - `element-builder.makeForwardElem` now emits a deflated
    `com.tencent.multimsg` LightApp JSON (LightAppElem, field 51)
    instead of the XML RichMsg (field 12). `forwardUuid` on the
    MessageElement is baked into `meta.detail.uniseq` and
    `extra.filename`. Auto-generates a UUID when absent.
  - `MessageElement.forwardUuid?: string` added for the round-trip.
  - `forward.uploadRecursive` threads `inner.uuid` into the outer's
    preview element AND uses the same uuid for the outer's
    piggyback actionCommand — closing the receiver-side lookup loop.
  - `forward.fetch` now walks ALL actions (not just `MultiMsg`),
    indexes piggybacks by uuid, and pre-populates the cache for
    each inner resId discovered in the outer's preview bubbles.
    Follow-up `fetch(innerResId)` hits the cache instead of the wire.
  - `rich-body-decoder` parses LightApp JSON; when `app ===
     com.tencent.multimsg`, emits `{type: 'forward', resId,
     forwardUuid}` so the OneBot layer can walk in. Legacy XML
     serviceID=35 path stays as a fallback for mobile QQ senders.

Wire-byte tests cover:
  - send: lightApp.data is 0x01-prefixed deflated JSON with the
    right meta + extra fields
  - send/nested: outer LongMsgResult carries `MultiMsg` AND a
    uuid-keyed piggyback; the piggyback uuid matches the
    preview's `meta.detail.uniseq` baked into the same outer
    msgBody
  - send/3-deep: outermost action[] has 1 + 2 entries (own
    MultiMsg + 2 piggybacks hoisted from deeper levels)
  - recv: lightApp `com.tencent.multimsg` → forward element with
    both resId and forwardUuid; non-multimsg LightApp falls
    through to {type:'json'}; legacy XML path still emits
    {type:'forward'}.

641 tests pass (was 628 + 13 new); typecheck clean; lint clean.
2026-05-23 12:45:26 +08:00
DrSmoothl 5724f490b4 refactor(oidb): thread ctx into serialize/deserialize for self-contained namespaces
The first pass at namespace-per-cmd left a smell: namespaces that
needed identity/uid resolution had to declare `Deps` AND then rewrite
their own spec inside `invoke` via a closure-over-resolved-value:

  // before
  export const serialize = (p: Params, targetUid: string) => ({...});
  export const invoke = async (deps, params) => {
    const targetUid = await deps.resolveUserUid(params.userId);
    return invokeOidb(deps, {
      ...Self,
      serialize: p => serialize(p, targetUid),  // ← escape hatch
    }, params);
  };

Reading "what does this cmd do on the wire?" required reading the
namespace AND the closure rewrite — the namespace wasn't really
self-contained, just a forwarder of two pure functions.

Fix: thread `ctx` (the namespace's full capability surface) into
`serialize` and `deserialize` directly. `serialize` may now return
`Promise<TReq>` so identity / uid lookups happen in place:

  // after
  export const serialize = async (ctx: Deps, p: Params) => ({
    groupUin: p.groupId, type: 1,
    body: {
      targetUid: await ctx.resolveUserUid(p.userId, p.groupId),
      duration: p.duration,
    },
  });
  export const invoke = (deps, params) => invokeOidb(deps, Self, params);

OidbCallSpec gains a `TCtx extends OidbSender` generic; invokeOidb's
first arg becomes `ctx: TCtx` (the sender capability is just a slice
of ctx now). buildOidbRequest / parseOidbResponse follow the same
shape. Async serialize is awaited inside invokeOidb.

17 namespaces had closure rewrites; all 17 collapse to plain
`invokeOidb(deps, Self, params)` invokes. Only one closure remains —
`fetch-user-profile` binds `params.uin` into deserialize as a server-
echo fallback; kept as a documented exception (the rare case where
deserialize wants request context, not ctx).

Wire bytes preserved across all migrations. 326 bridge tests + 302
core tests pass; lint clean.
2026-05-23 12:27:26 +08:00
DrSmoothl 99772851a7 chore(deps): add vitest coverage packages to devDependencies 2026-05-23 00:43:27 +08:00
DrSmoothl 9b4ff9c5e9 chore(lint): exempt oidb-services from no-namespace + clean stale directives
The namespace-per-cmd OIDB service layer was tripping
`@typescript-eslint/no-namespace` across 59 files; namespace IS
the OidbCallSpec by design (structural typing, one file per
(cmd, subcmd)) so disable the rule for that subtree only. Drop
four stale `eslint-disable-next-line @typescript-eslint/no-empty-
object-type` comments that aren't needed anymore (the empty
`Params {}` shape no longer triggers the rule), and fix an indent
nit in the get-unidirectional-friend-list test.

`npx eslint packages` now reports 0 errors. The 5 remaining
warnings are unused-vars / unused-eslint-disable in unrelated
pre-existing files.
2026-05-23 00:30:23 +08:00
DrSmoothl 521808a6eb refactor(oidb): migrate group-file.ts OIDB cmds to namespaces
Splits the 16 OIDB cmds from GroupFileApi into self-contained
namespaces under @snowluma/bridge/oidb-services/group-file/. Adds
@snowluma/bridge/oidb-services/shared with `toInt` / `ensureRetCodeZero`
helpers so namespaces can validate sub-responses without an
@snowluma/core dependency.

Boundaries:
  - Single-OIDB methods (publish / getCount / list-page / *url /
    delete / move / *folder) become one-line forwarders.
  - 3-stage methods (`upload` / `uploadPrivate`) keep their
    orchestration on the facade — OIDB preflight goes through the
    new namespace, but highway PUT + publish/sendC2cFile + upload-
    cache bookkeeping stay where they are.
  - `list` keeps its pagination loop on the facade; the page-fetch
    namespace is one call.
  - The NTV2-rich-media URL fetchers (group/private × ptt/video)
    share a deserialize helper (`parseNtv2DownloadUrl`) so all four
    namespaces only differ in their scene block.

Wire-byte equivalence preserved. The 0x6D6 family shares one
OidbGroupFileReq envelope; each namespace populates a different
slot (file / download / delete / move) and proton monomorphizes
per call site as before.
2026-05-23 00:22:59 +08:00
DrSmoothl 8d22868777 refactor(oidb): migrate group-admin.ts OIDB cmds to namespaces
Splits the 14 OIDB cmds from GroupAdminApi into self-contained
namespaces under @snowluma/bridge/oidb-services/group-admin/. The
facade becomes one-line forwarders; per-cmd capability is narrowed
to OidbSender (+ resolveUserUid / identity where the cmd actually
needs it).

Notes:
  - 0x89A_0 hosts MuteAll, SetAddOption, SetSearch, SetGroupName —
    same (cmd, subcmd) tuple, four different proto bodies; each gets
    its own namespace so proton can monomorphize independently.
  - 0x8A0_1 splits KickMember (single) and KickMembers (batch) for
    the same reason.
  - SetAddRequest carries uinForm=true and dynamic subCommand
    (filtered ? 2 : 1) on the same namespace.
  - GetAtAllRemain needs the bot's own uin → declares
    Pick<BridgeContext, 'identity'> in its Deps.

Wire-byte equivalence preserved; corner case for getAtAllRemain
empty-envelope path now returns {false, 0, 0} instead of throwing —
the new infrastructure normalises empty bodies and the OneBot action
ergonomics are unchanged.
2026-05-23 00:06:40 +08:00
DrSmoothl 014b87c7ce refactor(oidb): migrate contacts.ts OIDB cmds to namespaces
6 OIDB cmds moved; pagination + per-group cache + inflight
coalescing stay on the facade because they need cross-call state.

  - FetchFriendListPage       (0xFD4_1) — facade drives the cursor loop
  - FetchGroupList            (0xFE5_2) — uinForm; verbatim config blob
  - FetchGroupMemberListPage  (0xFE7_3) — facade drives the token loop +
                                          owns the TTL cache + inflight
                                          coalescing that keeps busy
                                          OneBot clients from tripping
                                          Tencent risk-control
  - FetchUserProfile          (0xFE1_2) — uinForm; full property-key
                                          catalogue cross-checked against
                                          LagrangeV2 FetchStrangerService.cs
  - FetchGroupRequests        (0x10C0_1/_2) — dynamic subcmd via filtered flag
  - FetchDownloadRkeys        (0x9067_202) — uinForm; respHead.retCode
                                             check moved into deserialize

35 new namespace-level tests covering serialize / deserialize edge
cases (property decoding, sex enum mapping, avatar URL suffix,
empty body handling, pagination cursor behaviour).

Verification:
  - typecheck green
  - @snowluma/bridge: 30 files / 233 tests
  - @snowluma/core: 302 tests
Wire bytes unchanged.
2026-05-22 23:51:33 +08:00
DrSmoothl 80afa31f5c refactor(oidb): migrate extras.ts OIDB cmds to namespaces
5 logical OIDB cmds → 4 namespace files (group todo's 3 sub-cmds
collapse into one namespace with a dynamic resolver):

  - GroupTodo          (0xF90_1/2/3) — set/complete/cancel via the
                                       new `action` discriminator
  - GetStrangerStatus  (0xFE1_2)     — uinForm=true; preserves the
                                       (status*10, ext_status) decode
                                       quirk verbatim
  - FetchAiVoiceList   (0x929D_0)
  - FetchAiVoice       (0x929B_0)    — single poll; retry loop
                                       stays on the facade so callers
                                       can plug their own back-off

Facade keeps the 6 public methods (setGroupTodo / completeGroupTodo /
cancelGroupTodo / getStrangerStatus / fetchAiVoiceList / fetchAiVoice)
as one-line forwarders; only `getStrangerStatus` wraps in try/catch
to preserve its "return null on transport failure" contract, and
`fetchAiVoice` keeps the polling loop.

30 new namespace-level tests; extras.test.ts moved onto the
sendRawPacket assertion model.

Verification:
  - typecheck green
  - @snowluma/bridge: 24 files / 195 tests
  - @snowluma/core: 302 tests
Wire bytes unchanged.
2026-05-22 23:45:13 +08:00
DrSmoothl 8356121518 refactor(oidb): migrate web.ts OIDB cmds to namespaces
2 OIDB cmds moved (the only OIDB calls in this file — the rest are
HTTP cookie / qun.qq.com / qzone web requests):

  - ForceFetchClientKey (0x102A_1) — used by ptlogin2 jump flow to
                                     swap clientKey → cookie jar
  - GetPskey            (0x102A_0) — p_skey fallback when ptlogin2
                                     omits it

The private `forceFetchClientKeyInner` / `getPSkey` helpers in
WebApi are now one-line forwarders into the namespaces. Facade
methods (`getCookies`, `getCsrfToken`, `getCredentials`, etc.) and
all HTTP-only cmds untouched.

12 new namespace-level tests covering deserialize edge cases
(empty server response, missing fields, dropped invalid items)
and e2e wire routing.

Verification:
  - typecheck green
  - @snowluma/bridge: 20 files / 165 tests
  - @snowluma/core: 302 tests
Wire bytes unchanged.
2026-05-22 23:40:17 +08:00
DrSmoothl 58e8081420 refactor(oidb): migrate misc.ts OIDB cmds to namespaces
3 OIDB cmds migrated; the 1 non-OIDB cmd (getMiniAppArk via
LightAppSvc) stays on the facade unchanged.

  - TranslateEnToZh           (0x990_2)
  - ClickInlineKeyboardButton (0x112E_1)
  - SendGroupSign             (0xEB7_1) — preserves historic UPPERCASE
                                          EB7 wire name via the new
                                          `wireName` override (the only
                                          cmd in the codebase with this
                                          quirk; every other cmd uses
                                          the default lowercase).

Tests:
  - misc.test.ts moved to bridge.sendRawPacket assertion model
  - 18 new namespace-level tests covering serialize, deserialize edge
    cases, magic constants, BigInt widening, and the EB7 case quirk

Verification:
  - typecheck green
  - @snowluma/bridge: 18 files / 153 tests
  - @snowluma/core: 302 tests
Wire bytes unchanged.
2026-05-22 23:37:41 +08:00
DrSmoothl f78431242c refactor(oidb): migrate friend.ts to namespaces
3 OIDB cmds moved to self-contained namespaces:
  - HandleFriendRequest (0xB5D_44) — digit-only flag still
                                     auto-resolves via resolveUserUid
  - DeleteFriend        (0x126B_0) — verbatim magic constants in the
                                     nested field2 sub-message
                                     (130/109/8/8/50 — server-side
                                     discriminator, MUST stay byte-
                                     equal to NTQQ wire)
  - SetFriendRemark     (0xB6E_2)

Cache invalidation (fetchFriendList after deletion) stays in the
facade — that side-effect needs `ctx.apis.contacts`, which namespaces
deliberately can't reach.

Tests migrated to assert against bridge.sendRawPacket. 15 new
namespace-level tests covering each invoke branch + serialize edge
cases.

Verification:
  - typecheck green
  - @snowluma/bridge: 15 files / 133 tests
  - friend.test.ts: 5/5
Wire bytes unchanged.
2026-05-22 23:33:22 +08:00
DrSmoothl a38f7ea26e refactor(oidb): migrate profile.ts OIDB cmds to namespaces
5 of profile.ts's methods were thin OIDB wrappers — migrated to the
new namespace pattern. The 5 non-OIDB methods (setOnlineStatus /
setDiyOnlineStatus / setAvatar / setGroupAvatar / fetchCustomFace)
stay on the facade unchanged — they hit `trpc.qq_new_tech.*`,
Highway, or `Faceroam.OpReq` directly and aren't in scope for the
OIDB-namespace refactor.

New namespaces (each with focused Pick<BridgeContext, …> deps):
  - SetProfile               (0x112A_2)  — needs identity for uin
  - SetSelfLongNick          (0x112A_2)  — same cmd different proto, ditto
  - SetInputStatus           (0xCD4_1)   — needs resolveUserUid
  - GetLike                  (0x7ED_12)  — needs identity + resolveUserUid
  - GetUnidirectionalFriendList (0xE17_0) — needs identity; non-standard
                                            wire name handled via the new
                                            wireName override on OidbCallSpec

Infrastructure tweak:
  - OidbCallSpec gains an optional `wireName(cmd, sub) → string`. Most
    cmds use the default `OidbSvcTrpcTcp.0xNNNN_N` scheme; 0xe17_0
    routes through `MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0`.

Test mock updates:
  - profile.test.ts moves the migrated cmds onto bridge.sendRawPacket
    mock (the new path bypasses the bridge-oidb runOidb mock). Non-OIDB
    cmds (setOnlineStatus, setAvatar, etc.) keep their existing mocks.
  - 36 new namespace-level tests in @snowluma/bridge covering serialize
    branches, deserialize edge cases, deps resolution, and e2e wire
    routing.

Verification:
  - pnpm -r typecheck: green
  - @snowluma/bridge: 12 files / 118 tests
  - @snowluma/core: 33 files / 302 tests
Wire shape unchanged across migrated cmds — same bytes produced.
2026-05-22 23:29:55 +08:00
DrSmoothl ff2b902cbd refactor(oidb): migrate interaction.ts remaining 3 cmds to namespaces
Completes the InteractionApi facade — every method is now a one-line
forwarder. New namespaces:
  - SendPoke   (0xED3_1)  — group + friend variants discriminated by
                            which of groupUin/friendUin is set
  - SendLike   (0x7E5_104) — thumbs-up to a target user
  - SetEssence (0xEAC_1/_2) — set/unset essence message, dynamic subcmd

Facade methods reduced to thin forwarders, mirror-fixed comment block
removed (the namespaces now own the wire-shape commentary).

Tests: 28 new in @snowluma/bridge covering each namespace's serialize
edge cases + dynamic subcmd resolution + e2e wire name + envelope body
shape. Cleaned the legacy `vi.mock('bridge-oidb')` setup out of
interaction.test.ts — the post-migration path doesn't route through
the mocked symbols anymore.

Wire behavior unchanged: every cmd produces the same bytes as before,
verified by encode round-trip in the unit tests.
2026-05-22 23:22:26 +08:00