- All Superpowers workflow artifacts for WeKit (plans, specs/designs, SDD ledgers and
reports, brainstorm sessions) are written, edited, and committed only in
~/coding/wekit_dev/superpowers(its own git repo; read itsAGENTS.mdfor layout and rules). Never create, edit, or commit.superpowers/ordocs/superpowers/inside this repo — those paths are gitignored here by design.
./x build # debug (uses same signing as release)
./x build --release # release (with optimization on)
./x run --zygisk # build the dual-format APK and install it as a Zygisk module
# (./x is alias to `cargo xtask` which orchestrates the build process)- When working in a Git worktree, initialize submodules before starting any work:
git submodule update --init --recursive. Worktrees do not automatically populate submodule contents, and builds will fail whenlibs/common/bshandlibs/common/reflektare empty. - When working in a Git worktree, work directly on
devunless the user explicitly requests another branch or isolated history. This is because commits made on a detached worktree are not automatically transferred by Codex's “local checkout” action and can appear to be lost. - Every standard/legacy debug/release APK is dual-format: install normally as an APK, or rename
to
.zipand install through a root manager. Module files enter the APK before AGP signs it; never modify or repack the signed output. There is no separatezygisk buildcommand. - JDK 21
- Gradle does NOT build the Rust native lib.
./gradlew assemble*only packages whatever prebuiltlibwekit_native.soalready sits inapp/src/main/jniLibs/<abi>/. Compilingapp/src/main/rust/wekit-nativeand refreshing those.sofiles is xtask's job (task_build_native), so always go through./x— running Gradle directly will silently ship a stale native lib. Requires a Rust toolchain + the Android NDK and its Rust targets;./x configureregenerateswekit-native/.cargo/config.tomlfrom the local NDK and is invoked automatically by the build tasks. ./x build --native-onlyprepares both the application and Zygisk native libs injniLibs/- AGP 9, Gradle version catalog in
gradle/libs.versions.toml
app/— main Android module, entrypoints, hooks, UI, native Rust liblibs/common/annotation-scanner/— KSP processors: source-subtype discovery forBaseFeature/ExtensionPackobjects plus the@AgentToolscannerlibs/common/libxposed-api/— compileOnly LibXposed API interface stubs (compileOnly since they are provided by user's Xposed framework)libs/common/bsh/— submodule: forked BeanShell interpreter with snapshot serialization (BshSnapshot,BshSnapshotHelper); snapshots are encrypted AST byte representations used by the WAuxiliary Xposed module;app/src/main/java/dev/ujhhgtg/wekit/utils/BshSnapshotDecompiler.kt— decompiles encrypted BeanShell snapshot files back into Java-like source code; the AES key was recovered from WAuxiliary's decompiled sourcelibs/common/reflekt/— submodule: reflection utility library (dev.ujhhgtg.reflekt)libs/common/stubs/— compileOnly stubs for WeChat and Android hidden classesbuildSrc/— custom Gradle tasks:GenerateMethodHashesTask(IResolveDexresolveDexmethod MD5 cache),GenerateNewFeaturesTask(Kotlin source files added within 30 days of the HEAD commit →NewFeatures.ADDED_AT_BY_SOURCE_KEY; KSP joins source keys to discovered features for the 新功能 pseudo-category)xtask/— build orchestration behind./x: native-lib compilation + NDK linker config, APK assembly/signing via Gradle, and Zygisk installation of that same APK
- Xposed entry:
dev.ujhhgtg.wekit.loader.entry.lxp.LxpHookEntry(libxposed 101 ~ 102) and legacy Xposed API (51+) entry:dev.ujhhgtg.wekit.loader.entry.xp51.Xp51HookEntry - Unified flow:
UnifiedEntryPoint.entry()→StartupAgent.startup()→WeLauncher.init() - Feature objects inherit
BaseFeature, declaretechnicalId/resource/category metadata as override properties, and are auto-discovered by KSP from their source subtype at compile time - Extension pack objects implement
ExtensionPack, declare a requireddisplayOrder, and are auto-discovered by the same KSP processor - Base classes:
SwitchFeature(toggle on/off),ClickableFeature(toggle on/off with onClick event),ApiFeature(always-on),BaseFeature(abstract base, do not use directly) - DEX analysis via DexKit with
IResolveDexinterface; method resolve body MD5-hashed for cache (GenerateMethodHashesTask) - DEX-resolved targets DSL:
val methodTarget by dexMethod()val classTarget by dexClass()delegate →methodTarget.hookBefore { ... },val method: Method = methodTarget.method,val clazz = classTarget.clazz - UI: Jetpack Compose + Material 3, dialogs written using
showComposeDialogandAlertDialogContent; settings screens follow the Material 3 UI Standards section below (ui/content/m3/widget family, InstallerX-Revived design) - Config:
KvStore(Room-backed key-value store, legacy MMKV migrated viaMmkvReadonlyReader) - Logging: via
WeLogger
- Use
./x dex-testto run the sameIResolveDex/DexKit resolution steps used byDexCacheManager.ktagainst WeChat APKs on the Linux desktop. Test only the supported host range 8.0.65–8.0.78; APKs outside that range are useful for investigation but must not be treated as compatibility gates for the project. - Add
--workers 4to exercise the same shared batch scheduler as local Android resolution. Use--workers 1for its serial comparison; omitting the option preserves per-feature runs. Batch runs initialize/reset all selected roots before starting workers and report after joining. - Test each supported APK version separately, including separate normal and Google Play APKs when both are available. Each APK runs in its own JVM worker and must carry its own version code, version name, build tag, and Google Play metadata.
- Reports belong under
dex-test-results/<run-id>/(or an explicitly supplied output directory), never under Gradle'sbuild/reports/. Preserve the per-APK JSON reports and aggregate summary. - Resolution classification is strict: an
allowFailure = truedelegate that receives its placeholder isEXPECTED_FAILURE; an unhandled resolver exception isUNEXPECTED_FAILURE; delegates that remain pending after that exception areBLOCKEDand must record the triggering delegate; a resolver returning with pending delegates isINCOMPLETE. - A desktop resolution pass does not prove hook-time behavior on a physical device. Initialization, worker, native-library, APK metadata, report, unexpected, blocked, or incomplete failures must remain visible and make the command fail.
- DexKit desktop testing is intentionally expensive. After a supported-version run has passed,
do not rerun it for unrelated changes when no Dex declarations or resolution steps changed.
Rerun the affected supported APK versions after changing
dexMethod,dexClass,dexField, inline matchers, or the correspondingresolveDex/resolveInlineDexlogic. - Before reporting a Dex resolver change as complete, run the affected desktop tests plus any
relevant existing or qualifying Gradle tests (as defined under Testing Strategy),
./x build, andgit diff --check.
resolveDex,resolveInlineDex, and inline matcher blocks run in the sameDexResolutionContext. When a matcher needs information from an already-resolved delegate, use its DexKit metadata (delegate.data.name,.declaredClassName,.returnTypeName,.paramTypeNames,.superClass,.interfaces, etc.), not JVM reflection. In particular, do not use another delegate's.clazz,.method,.constructor,.field,asClass, or reflection-derivedClass/type information to construct a later Dex query: desktop workers cannot reliably load WeChat/Android classes.- Do not hide that reflection behind a
lazyproperty or object initialization. A resolver-side lazy such asby lazy { target.method.declaringClass }is still invalid for desktop testing; derive the required descriptor fromtarget.datawhile resolving instead. Reflection properties remain valid after resolution for actual hook-time Android behavior; this rule applies only to declaration and resolution paths. - An explicitly user-approved host-version branch, or any build-tag/Google Play branch, inside
resolution must read
DexResolutionContext.host, rather thanHostInfo, so./x dex-testuses metadata belonging to the APK under test. Android resolution receives equivalent current-host metadata through the same context. - A metadata migration must preserve the intended descriptor/matcher constraints. Do not loosen strings, signatures, or structural predicates merely to make a desktop test pass; use stable DexKit evidence as normal.
- For an intentional supported-version absence, use
allowFailure = trueonly as documented below. Its generated generic expected-failure reason is acceptable; provide a more precise reason when it materially clarifies a structure-selected compatibility path. Do not convert exceptions or uncertain matches into placeholders just to obtain a green report. - Resolver source is part of the device cache key: even a mechanically equivalent rewrite from
reflection to
.datachanges the generatedmethodHashand invalidates that feature's old cache. Expect one device re-resolution after such a change; never retain or hand-edit an old hash to suppress it. Avoid unrelated formatting/refactors in resolver and inline matcher bodies when a cache invalidation is not intended.
- Prefer structure-based compatibility over host-version checks. In Dex resolution, first probe one stable class, method, field, or constructor that exists only on the newer path. If that probe produces zero results, record its expected placeholder and fall back to the older path. If it is present, keep every other required target on that path strict. Multiple results, matcher errors, and failures after the probe must remain visible failures; they are not fallback conditions.
- At hook time or when invoking resolved host members, choose the path from the actual resolved
structure. Use the new-path probe's
isPlaceholder, inspect the resolved member's reflection signature, or test another directly relevant runtime property. Do not repeat the resolver's compatibility decision with a host-version comparison. - If old and new hosts expose the same semantic member with only a signature difference, accept
the confirmed signatures structurally (for example,
paramCount(10, 11)). When invocation arguments differ, inspectMethod.parameterCountorConstructor.parameterCountand construct the arguments from that actual signature. - Avoid branches based on the WeChat host's
versionCode,versionName, hard-coded WeChat version strings, or equivalent version constants. If a host-version check is genuinely unavoidable, ask the user for explicit confirmation before adding or retaining it. Distinguishing a Google Play build throughisHostGooglePlay/isGooglePlayis not a host-version check and does not require that confirmation.
-
These repository-specific testing constraints take precedence over the generic Superpowers skills' TDD workflow. Do not add tests for host hooks, Compose UI, WeChat runtime behavior, or database integration when they fall outside the qualifying conditions below; use the required build, static checks, and manual host validation instead.
-
TDD and new automated tests are allowed only when all core logic under test lives in WeKit, has low coupling to WeChat, and does not depend on WeChat host classes, runtime state, UI, or behavior.
-
Do not add tests for simple logic that is easy to verify by static review, such as constants, direct mappings, boolean expressions, identity functions, or straightforward arithmetic. Do not add tests merely to satisfy a workflow or a skill such as Superpowers.
-
Do not increase production-code complexity to create a test seam. In particular, do not split a simple object singleton into an interface plus implementation, introduce unnecessary wrappers or dependency injection, or extract simple one-use logic into a standalone function solely so it can be unit-tested.
-
Keep simple logic inline when it has only one use and does not form a meaningful reusable domain boundary. Extract a helper only when it improves readability, is reused, or isolates genuinely complex behavior; testability alone is not sufficient justification.
-
If work does not meet all of those conditions, do not use TDD and do not add low-value tests merely to satisfy a testing workflow. Host hooks, reflection/DexKit glue, and host UI behavior are normally in this category.
-
Use
./x dex-testfor automated Dex resolution validation as documented above. Apart from Dex resolution, manual testing in the real WeChat host is the primary behavioral test method; desktop JVM or Gradle tests do not replace it.
- Package namespace:
dev.ujhhgtg.wekit appis an application module, not a library and cannot be consumed by other projects. Do not use theinternalvisibility modifier in Kotlin production sources underapp/src/main; use Kotlin's default implicitpublicvisibility instead, becauseinternalprovides no meaningful boundary there.- Min SDK 28, target SDK 37, compile SDK 37
- Target: WeChat
com.tencent.mm, versions 8.0.65–8.0.78. Current host info inHostInfo - Process targeting via
TargetProcesses: overridestartup()to checkTargetProcesses.isInMain/TargetProcesses.currentType. Default: main process only. - Device behavior still requires manual testing on real WeChat; desktop JVM tests cover Dex resolution only and do not replace device validation.
- NEVER wrap
hookBeforeandhookAfterin atry-catch/runCatchingblock. They should NOT fail. If they fail, then it's the module developer's problem. - Use
KvStore.prefOptiondelegates to declare & use preference items easily. - Teardown/revert on
onDisableis best-effort by design, not a requirement. Many features irreversibly modify the host view tree; fully reverting them would need complex state management and syncing for little gain, so having the user restart WeChat is the accepted approach. Do NOT report "feature does not undo its changes inonDisable" as a bug. allowFailureondexMethod/dexClass/dexFieldis ONLY for structures whose existence differs across supported WeChat versions (present in old, absent in new, or vice versa). If a declared Dex resolution is expected to succeed on every supported version, do NOT setallowFailure: a resolution failure must fail that feature loudly instead of silently degrading to a no-op.- JVM reflection over host classes should go through
reflekt(libs/common/reflekt/) by default, e.g.thisObject.reflekt().firstField { ... }or.getField(name, true)— not hand-rolledgetDeclaredField/getMethodtraversal. - NEVER use
Path.oforFiles.writeString. These are frequent mistakes and are unavailable on older Android API levels supported by WeKit. Convert strings throughdev.ujhhgtg.wekit.utils.fs.asPathfromutils/fs/PathUtils.kt(for example,pathString.asPathorbase.asPath.resolve(child)) and write text throughkotlin.io.path.writeText. - No excessive defensiveness. When e.g. the hooked method and its argument types are
known to hold, use direct casts:
thisObject as Activity,args[0] as View,!!. Do NOT useas?safe casts,args.getOrNull(0),?:,?.someFun()or similar guards for values that should always be present/non-null/etc. Code that is correct does not need the defense; code that is wrong must throw loudly and get caught by eitherHookUtils' or code's own exception catcher, and these guards only swallow the exception and hide the real error. Defenses and guards that are reasonable should still exist. - The libraries
DexKitandreflektare NOT something you are familiar with. Do NOT hallucinate their API surfaces. Read their code before using them. - In Compose,
LocalContextalways means the platform context and is never localized by WeKit. Use standard Compose resource APIs for composable text andLocalWeKitLocalizedContextonly for imperative WeKit resource reads. Mixed platform/resource operations must read both locals. UseLocalActivity.currentfor Activity-only APIs, and never add AndroidX owner forwarding toWeKitLocaleProvider.
Design reference: ~/coding/InstallerX-Revived — when unsure how a settings page should
look or behave, read its app/src/main/java/com/rosan/installer/ui/page/main/widget/setting/.
WeKit's ported widget family lives in app/src/main/java/dev/ujhhgtg/wekit/ui/content/m3/.
- Settings screens are a
LazyColumnofSegmentedColumngroups (inset rounded cards, one group per concern, shorttitleabove each group). Do not hand-roll card layouts or use flat lists with dividers. - Use the shared scaffolds —
M3ListScaffold(activity/settings/SettingsActivity.kt) orSettingsScaffold(ui/content/m3/SettingsComponents.kt): collapsingLargeFlexibleTopAppBar+ blur + back button. Do not build per-screen scaffolds. - Multi-screen settings follow the miuix-nav
NavDisplaypattern ofWeAgentSettingsActivity/ReadReceiptsSettingsActivity(sealed@Serializableroutes, predictive-back drill-down).
Prefer these over raw Compose controls:
- Plain / status / navigation row →
BaseWidget(chevron or action intrailingContent). - Boolean setting →
SwitchWidget. - Exclusive choice →
RadioButtonWidget; supports dual click areas like the WeAgent "Memory" row:onClick(main area, e.g. opens the detail screen) +onSelect(the radio itself) +trailingDivider. - String or number input →
TextFieldDialogWidget: a standard clickable row showing the current value that edits it in a dialog with cancel/confirm. Or for draft & save semantics, place a bareTextField/OutlinedTextFielddirectly in aBaseSupportingWidget. - Value with a natural range and step (counts, seconds, delays) →
IntNumberPickerWidget(slider row with drag tooltip), wrapped in aBaseItemContainerinside the group. Ports, hostnames, tokens, URLs and other free-form identifiers have no slider semantics — use the dialog row instead. - Compact choice from a fixed set →
DropDownMenuWidget.
- Prefer instant apply: toggles, radios, sliders and dialog confirmations commit on change. Avoid "draft state + Save button" page designs — a text row's dialog cancel/confirm is its only draft lifecycle. Genuinely transactional flows (connect / verify / disconnect) may keep explicit action buttons; those are actions, not saves.
- Buttons that belong together share ONE row (
Modifier.weight(1f)each, ~12dp gap) — not one button per line. Pair an action with its opposite (connect/disconnect, save/delete); destructive actions use the error color and a confirm dialog. - While an operation is in flight, disable the affected rows and show an inline progress/feedback line; never leave conflicting controls tappable.
- Blank values show a hint in the row description; the row itself stays clickable.
- 群聊: WeChat: chatroom; WeKit: group/群组
- 朋友圈: WeChat: sns; WeKit: moment
- WeChat decompiled sources: ~/coding/wechat_80{65,67,69,74,76}
- Decrypted WeChat main database: ./decrypted_wechat.db
- GitHub Actions: builds on push/PR to
master/dev(skips non-code changes) - Artifacts automatically published to a release named "CI" + Telegram channel