chainlesschain 0.162.14 → 0.162.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-D34d_Nh1.js → AIOps-Dr5poTWh.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Br7HxCnl.js → ActionButton-DLOGPHQ-.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-bVKq79Xd.js → Analytics-DBlO3FPX.js} +1 -1
  5. package/src/assets/web-panel/assets/{AppLayout-CWSLIbAz.js → AppLayout-BrMarmWx.js} +2 -2
  6. package/src/assets/web-panel/assets/{Audit-Cmnu1qqa.js → Audit-DX6omFqA.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-Rok20-TL.js → Backup-0QSLlQkB.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-BJzs_ZtT.js → BaseInput-Dn4GFfR8.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-CSYapbcq.js → Chat-CMTeA0Zo.js} +1 -1
  10. package/src/assets/web-panel/assets/{Checkbox-BEa7Sr7e.js → Checkbox-BQQxf6iA.js} +1 -1
  11. package/src/assets/web-panel/assets/{Codegen-C9M4e7ne.js → Codegen-D2vrazFC.js} +1 -1
  12. package/src/assets/web-panel/assets/{Col-DU9NoUIi.js → Col-P1hVDVKm.js} +1 -1
  13. package/src/assets/web-panel/assets/{Community-DA9uz_jP.js → Community-CPcDkABv.js} +1 -1
  14. package/src/assets/web-panel/assets/{Compact-3_bEraVw.js → Compact-DvBQwE-b.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compliance-BtF8jWUQ.js → Compliance-GX2qsHkq.js} +1 -1
  16. package/src/assets/web-panel/assets/{Cowork-BqvA7oaM.js → Cowork-CEEISrBb.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cron-CxZy7Mzg.js → Cron-Y3D6wB4C.js} +1 -1
  18. package/src/assets/web-panel/assets/{Crosschain-1DB-XRGu.js → Crosschain-CnCjDKKD.js} +1 -1
  19. package/src/assets/web-panel/assets/{DID-B6Ezp1pt.js → DID-CJ2EFB5b.js} +1 -1
  20. package/src/assets/web-panel/assets/{Dashboard-QDJ6VVsn.js → Dashboard-CWujrs70.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dropdown-CovsWjxG.js → Dropdown-BzKSQA5P.js} +1 -1
  22. package/src/assets/web-panel/assets/{Federation-DbRxS4Y4.js → Federation-DgaAWIwd.js} +1 -1
  23. package/src/assets/web-panel/assets/{FormItemContext-9E9dNGtx.js → FormItemContext-BitgB4Gb.js} +1 -1
  24. package/src/assets/web-panel/assets/{Git-CqEpyxRZ.js → Git-Bzj4vLLf.js} +1 -1
  25. package/src/assets/web-panel/assets/{Governance-On47KtGq.js → Governance-D7DEdSBD.js} +1 -1
  26. package/src/assets/web-panel/assets/{Inference-RZcjcyaq.js → Inference-CVsGLfcX.js} +1 -1
  27. package/src/assets/web-panel/assets/{KnowledgeGraph-C-1rRAM9.js → KnowledgeGraph-CF6C6ZNo.js} +1 -1
  28. package/src/assets/web-panel/assets/{Logs-BuunmG_r.js → Logs-C0Er-tt8.js} +1 -1
  29. package/src/assets/web-panel/assets/{Marketplace-CromymyA.js → Marketplace-_E7uwUUU.js} +1 -1
  30. package/src/assets/web-panel/assets/{McpTools-5XlFExh7.js → McpTools-dHbktLMV.js} +1 -1
  31. package/src/assets/web-panel/assets/{Memory-DjnUT7YM.js → Memory-CaQ4UY7b.js} +1 -1
  32. package/src/assets/web-panel/assets/{MobileBridge-BrYIgLg6.js → MobileBridge-BbOh0uJt.js} +1 -1
  33. package/src/assets/web-panel/assets/{MobileProjects-CL5V3fTm.js → MobileProjects-CfdNEimZ.js} +1 -1
  34. package/src/assets/web-panel/assets/{Mtc-CHYJq6zK.js → Mtc-HLvyepRP.js} +1 -1
  35. package/src/assets/web-panel/assets/{MtcAudit-BZxUO0qt.js → MtcAudit-DMaHGoIr.js} +1 -1
  36. package/src/assets/web-panel/assets/{Multisig-FZTmJgW1.js → Multisig-DFNZf0AB.js} +1 -1
  37. package/src/assets/web-panel/assets/{NLProgramming-C9Mhefph.js → NLProgramming-cZwXhWy2.js} +1 -1
  38. package/src/assets/web-panel/assets/{Notes-W7usj-Ar.js → Notes-U_3n8Zid.js} +1 -1
  39. package/src/assets/web-panel/assets/{NotificationSettings-PBuYv_Bh.js → NotificationSettings-B4VvhkKf.js} +1 -1
  40. package/src/assets/web-panel/assets/{Organization-CuYCE-rF.js → Organization-BIwBFrdX.js} +1 -1
  41. package/src/assets/web-panel/assets/{Overflow-Dojx-kzE.js → Overflow-_zxR8gF2.js} +1 -1
  42. package/src/assets/web-panel/assets/{P2P-BgIaSrLX.js → P2P-DJXmWhLC.js} +1 -1
  43. package/src/assets/web-panel/assets/{Permissions-Byj2dkF_.js → Permissions-DwJ35K8Z.js} +1 -1
  44. package/src/assets/web-panel/assets/{PersonalDataHub-CMOOI13-.js → PersonalDataHub-CzZXLXGN.js} +1 -1
  45. package/src/assets/web-panel/assets/{Pipeline-CWwEOF09.js → Pipeline-DjtRGM8X.js} +1 -1
  46. package/src/assets/web-panel/assets/{Privacy-VT7gldcN.js → Privacy-BJhsIhGf.js} +1 -1
  47. package/src/assets/web-panel/assets/{ProjectInit-7UH3c3p7.js → ProjectInit-DEx5SDLh.js} +1 -1
  48. package/src/assets/web-panel/assets/{ProjectSettings-DqLp-72a.js → ProjectSettings-B6qDg7nn.js} +1 -1
  49. package/src/assets/web-panel/assets/{Projects-B_54eDhH.js → Projects-CKkTgrBf.js} +1 -1
  50. package/src/assets/web-panel/assets/{Providers-BIrNfNpc.js → Providers-3GEshcW_.js} +1 -1
  51. package/src/assets/web-panel/assets/{QuickAsk-BbYPwCso.js → QuickAsk-BfUXbKxa.js} +1 -1
  52. package/src/assets/web-panel/assets/{Recommend-BF4qBssF.js → Recommend-DgxYasSJ.js} +1 -1
  53. package/src/assets/web-panel/assets/{Reputation-DPEzlC2V.js → Reputation-CWvVg_V1.js} +1 -1
  54. package/src/assets/web-panel/assets/{Row-DjHxhH1L.js → Row-42M5ot1v.js} +1 -1
  55. package/src/assets/web-panel/assets/{RssFeed-D0_j678P.js → RssFeed-Do3isL1x.js} +1 -1
  56. package/src/assets/web-panel/assets/{Search-DctfGehu.js → Search-_7wgwjaR.js} +1 -1
  57. package/src/assets/web-panel/assets/{Security-BFHggeYM.js → Security-DKB6pe8t.js} +1 -1
  58. package/src/assets/web-panel/assets/{Services-CmrFMukV.js → Services-BHrBJAcB.js} +1 -1
  59. package/src/assets/web-panel/assets/{Skeleton-DR4vn_nS.js → Skeleton-C8L-EfGp.js} +1 -1
  60. package/src/assets/web-panel/assets/{Skills-DlXG2yyV.js → Skills-CU66huTh.js} +1 -1
  61. package/src/assets/web-panel/assets/{Sla-4PPGL3SE.js → Sla-DA8AKNmI.js} +1 -1
  62. package/src/assets/web-panel/assets/{SpeechSettings-D9EhJOqm.js → SpeechSettings-CufjYTzV.js} +1 -1
  63. package/src/assets/web-panel/assets/{SyncSettings-Dasmbi0p.js → SyncSettings-D_VAYHIg.js} +1 -1
  64. package/src/assets/web-panel/assets/{Tasks-vilEiuPA.js → Tasks-BE6nYh9k.js} +1 -1
  65. package/src/assets/web-panel/assets/{Templates-Ca9Rvktn.js → Templates-BfRbttd6.js} +1 -1
  66. package/src/assets/web-panel/assets/{Tenant-CEZb9gfK.js → Tenant-YO71CL80.js} +1 -1
  67. package/src/assets/web-panel/assets/{Terminal-DanCBdbD.js → Terminal-D1mQaE_Z.js} +1 -1
  68. package/src/assets/web-panel/assets/{Tokens-SPkClW2d.js → Tokens-CvnNQtX4.js} +1 -1
  69. package/src/assets/web-panel/assets/{Trigger-B645yL7g.js → Trigger-BnN19FJt.js} +1 -1
  70. package/src/assets/web-panel/assets/{Trust-D9sM_Ig0.js → Trust-C6oZLHqp.js} +1 -1
  71. package/src/assets/web-panel/assets/{UkeySign-B_Nr2K-u.js → UkeySign-BOhFYuWu.js} +1 -1
  72. package/src/assets/web-panel/assets/{VideoEditing-U01Lea8j.js → VideoEditing-NMc6-qc-.js} +1 -1
  73. package/src/assets/web-panel/assets/{Wallet-6xBySVV8.js → Wallet-BALmcKtd.js} +1 -1
  74. package/src/assets/web-panel/assets/{WebAuthn-DbgMoBu6.js → WebAuthn-6IVe-W6O.js} +1 -1
  75. package/src/assets/web-panel/assets/{WorkflowEditor-Bz-Y6IR2.js → WorkflowEditor-BIv20DXb.js} +1 -1
  76. package/src/assets/web-panel/assets/{chat-BC_O9hag.js → chat-CiceyA69.js} +1 -1
  77. package/src/assets/web-panel/assets/{colors-ChlOGOvr.js → colors-CxwvpRW0.js} +1 -1
  78. package/src/assets/web-panel/assets/{compact-item-BSbAYGGF.js → compact-item-N6ASDseQ.js} +1 -1
  79. package/src/assets/web-panel/assets/{createContext-CFcZly5M.js → createContext-CQ8PEhyP.js} +1 -1
  80. package/src/assets/web-panel/assets/{hasIn-BomYwwYE.js → hasIn-DDiIuryC.js} +1 -1
  81. package/src/assets/web-panel/assets/{index-DXgE2VW6.js → index-1jBrqw2R.js} +1 -1
  82. package/src/assets/web-panel/assets/index-1losWCP0.js +1 -0
  83. package/src/assets/web-panel/assets/{index-DNkth8dM.js → index-92sMXPmR.js} +1 -1
  84. package/src/assets/web-panel/assets/{index-BHGsFwYW.js → index-BCgZTQf8.js} +1 -1
  85. package/src/assets/web-panel/assets/{index-gUACAWbM.js → index-BKLatStI.js} +1 -1
  86. package/src/assets/web-panel/assets/{index-CoF95pYK.js → index-BKmY57ry.js} +1 -1
  87. package/src/assets/web-panel/assets/{index-DW-Ji07y.js → index-BZ4O1Vp7.js} +1 -1
  88. package/src/assets/web-panel/assets/{index-onW325hZ.js → index-BdmcwwMp.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-CnxlKTDK.js → index-Bj-mAQFK.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-BI1jAWcc.js → index-BjvirvV4.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BEJ6YiLI.js → index-Bw6UqIkJ.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-CCRSz2cR.js → index-BzdEj9_B.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-slYX2rCE.js → index-C43WIe_p.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BGUbtM3R.js → index-CHsNn-Qv.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CBhoZhCO.js → index-COIxnEwP.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BHi69MHF.js → index-C_dbCu-F.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-b8GbH2Yi.js → index-Cb0QVAZL.js} +3 -3
  98. package/src/assets/web-panel/assets/{index-za1GUJBG.js → index-CcUK2M7W.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-T5Y_9IPv.js → index-ClASVywF.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-B6LJHQoE.js → index-CnArbjXg.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BZGdjNLA.js → index-Cur_KKpV.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Ctx97mH-.js → index-Cwk0olXV.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DsNQ2hqI.js → index-D0yG93O4.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-5Ewm6KZA.js → index-D58a5SL3.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BwFykZ5U.js → index-DEjAgx2R.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Cj47XwJQ.js → index-DUKjS4kF.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-C0rr1X9W.js → index-DWKigrAM.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-EY733h9z.js → index-DcMESTJs.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DRp5_Xns.js → index-DgwpVvQq.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Cmzh8gKL.js → index-Dj2wgF3A.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-ozVPr1gj.js → index-DobYLmfZ.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-ByZQNO0A.js → index-Dp3r80qO.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Dbf5YmDX.js → index-DtpWqJ-2.js} +1 -1
  114. package/src/assets/web-panel/assets/index-EoP_WtDt.js +1 -0
  115. package/src/assets/web-panel/assets/{index-D0vX9jQA.js → index-_BHtKVC_.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-t9u2bHpH.js → index-bYorCa3r.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BYDvb1pi.js → index-hn9LVkIY.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-D_0B3CiU.js → index-mmpauJ3E.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-QD_n54XT.js → index-xQlQRo2j.js} +1 -1
  120. package/src/assets/web-panel/assets/{initDefaultProps-DnadEaxu.js → initDefaultProps-1MKZtqXZ.js} +1 -1
  121. package/src/assets/web-panel/assets/{motion-CC_Na0Tl.js → motion-DFBQpRS8.js} +1 -1
  122. package/src/assets/web-panel/assets/{move-C2d9Mkk9.js → move-CiI8Ada0.js} +1 -1
  123. package/src/assets/web-panel/assets/{omit-QvpKbF8p.js → omit-CjeFoPcC.js} +1 -1
  124. package/src/assets/web-panel/assets/{pickAttrs-Dm8r3X1_.js → pickAttrs-Be3kefJq.js} +1 -1
  125. package/src/assets/web-panel/assets/{placementArrow-DaqaVfoX.js → placementArrow-Be5Ra1_B.js} +1 -1
  126. package/src/assets/web-panel/assets/{responsiveObserve-Iida9fIn.js → responsiveObserve-NCu3YHiX.js} +1 -1
  127. package/src/assets/web-panel/assets/{slide-YqHexXQD.js → slide-f23lSB4X.js} +1 -1
  128. package/src/assets/web-panel/assets/{statusUtils-BGKLoeEt.js → statusUtils-Dq99US_U.js} +1 -1
  129. package/src/assets/web-panel/assets/{styleChecker-aI-gsQO8.js → styleChecker-B-UUq5Ww.js} +1 -1
  130. package/src/assets/web-panel/assets/{useFlexGapSupport-BiOsz4rc.js → useFlexGapSupport-D6aUzeVO.js} +1 -1
  131. package/src/assets/web-panel/assets/{useFs-CZy7Zo2X.js → useFs-DU-R5a4I.js} +1 -1
  132. package/src/assets/web-panel/assets/{vnode-B6WqjmE4.js → vnode-N7r8LSGe.js} +1 -1
  133. package/src/assets/web-panel/assets/{zoom-DTeTrJ2z.js → zoom-07xoxB1t.js} +1 -1
  134. package/src/assets/web-panel/index.html +1 -1
  135. package/src/commands/__tests__/hub-wechat.test.js +186 -15
  136. package/src/commands/hub.js +206 -2
  137. package/src/lib/__tests__/cc-android-bridge.test.js +245 -0
  138. package/src/lib/cc-android-bridge.js +99 -55
  139. package/src/lib/personal-data-hub-wiring.js +24 -2
  140. package/src/assets/web-panel/assets/index-BIRYt1of.js +0 -1
  141. package/src/assets/web-panel/assets/index-CZfySmWX.js +0 -1
@@ -41,19 +41,27 @@ function fakeHub(overrides = {}) {
41
41
  ...overrides.register,
42
42
  };
43
43
  }),
44
- listWechatAccounts: vi.fn(() => overrides.list || [
45
- {
46
- uin: "1234567890",
47
- dbPath: "/tmp/EnMicroMsg.db",
48
- hasWechatDataPath: true,
49
- chosenKeyProvider: "md5",
50
- registeredAt: 1716280000000,
51
- lastSyncAt: null,
52
- },
53
- ]),
44
+ listWechatAccounts: vi.fn(
45
+ () =>
46
+ overrides.list || [
47
+ {
48
+ uin: "1234567890",
49
+ dbPath: "/tmp/EnMicroMsg.db",
50
+ hasWechatDataPath: true,
51
+ chosenKeyProvider: "md5",
52
+ registeredAt: 1716280000000,
53
+ lastSyncAt: null,
54
+ },
55
+ ],
56
+ ),
54
57
  unregisterWechatAdapter: vi.fn(async (uin) => {
55
58
  if (!uin) return { ok: false, reason: "UIN_REQUIRED" };
56
- return { ok: true, removed: uin === "1234567890", uin, ...overrides.unregister };
59
+ return {
60
+ ok: true,
61
+ removed: uin === "1234567890",
62
+ uin,
63
+ ...overrides.unregister,
64
+ };
57
65
  }),
58
66
  };
59
67
  }
@@ -106,9 +114,21 @@ describe("cc hub wechat register", () => {
106
114
  _internal.cmdWechatRegister({ _getHub: async () => hub, json: true }),
107
115
  ).rejects.toThrow(/process\.exit/);
108
116
  const allOut =
109
- logSpy.mock.calls.map((c) => c.map((p) => (typeof p === "string" ? p : JSON.stringify(p))).join(" ")).join("\n") +
117
+ logSpy.mock.calls
118
+ .map((c) =>
119
+ c
120
+ .map((p) => (typeof p === "string" ? p : JSON.stringify(p)))
121
+ .join(" "),
122
+ )
123
+ .join("\n") +
110
124
  "\n" +
111
- errSpy.mock.calls.map((c) => c.map((p) => (typeof p === "string" ? p : JSON.stringify(p))).join(" ")).join("\n");
125
+ errSpy.mock.calls
126
+ .map((c) =>
127
+ c
128
+ .map((p) => (typeof p === "string" ? p : JSON.stringify(p)))
129
+ .join(" "),
130
+ )
131
+ .join("\n");
112
132
  expect(allOut).toMatch(/--uin|uin/i);
113
133
  });
114
134
 
@@ -217,7 +237,9 @@ describe("cc hub wechat list", () => {
217
237
  describe("cc hub wechat unregister", () => {
218
238
  it("calls hub with uin + reports removed", async () => {
219
239
  const hub = fakeHub();
220
- await _internal.cmdWechatUnregister("1234567890", { _getHub: async () => hub });
240
+ await _internal.cmdWechatUnregister("1234567890", {
241
+ _getHub: async () => hub,
242
+ });
221
243
  expect(hub.unregisterWechatAdapter).toHaveBeenCalledWith("1234567890");
222
244
  const out = logSpy.mock.calls.map((c) => c.join(" ")).join("\n");
223
245
  expect(out).toMatch(/removed wechat account.*1234567890/);
@@ -238,6 +260,155 @@ describe("cc hub wechat unregister", () => {
238
260
  });
239
261
  const out = logSpy.mock.calls.map((c) => c[0]).join("\n");
240
262
  const parsed = JSON.parse(out);
241
- expect(parsed).toEqual(expect.objectContaining({ ok: true, removed: true, uin: "1234567890" }));
263
+ expect(parsed).toEqual(
264
+ expect.objectContaining({ ok: true, removed: true, uin: "1234567890" }),
265
+ );
266
+ });
267
+ });
268
+
269
+ // ─── Phase 12.9 — `cc hub wechat doctor` ──────────────────────────────
270
+
271
+ describe("interpretWechatProbe (pure)", () => {
272
+ const { interpretWechatProbe } = _internal;
273
+
274
+ it("device unreachable → blocked + adb hint", () => {
275
+ const r = interpretWechatProbe({
276
+ device: { reachable: false },
277
+ wechat: {},
278
+ root: {},
279
+ frida: {},
280
+ });
281
+ expect(r.readiness).toBe("blocked");
282
+ expect(r.blockers.join(" ")).toMatch(/adb/);
283
+ expect(r.nextSteps.join(" ")).toMatch(/adb devices/);
284
+ });
285
+
286
+ it("WeChat not installed → blocked + install hint", () => {
287
+ const r = interpretWechatProbe({
288
+ device: { reachable: true },
289
+ wechat: { installed: false },
290
+ root: {},
291
+ frida: {},
292
+ });
293
+ expect(r.readiness).toBe("blocked");
294
+ expect(r.blockers.join(" ")).toMatch(/com\.tencent\.mm/);
295
+ });
296
+
297
+ it("WeChat < 8 + md5 path → ready with adb pull instructions", () => {
298
+ const r = interpretWechatProbe({
299
+ device: { reachable: true },
300
+ wechat: { installed: true, majorVersion: 7, versionName: "7.0.22" },
301
+ root: { detected: false },
302
+ frida: {},
303
+ suggestedKeyProvider: "md5",
304
+ });
305
+ expect(r.readiness).toBe("ready");
306
+ expect(r.nextSteps.some((s) => /adb pull/.test(s))).toBe(true);
307
+ expect(r.nextSteps.some((s) => /cc hub wechat register/.test(s))).toBe(
308
+ true,
309
+ );
310
+ // non-root warning when MD5 path lacks root (backup subset only)
311
+ expect(r.warnings.some((w) => /非 root|backup 子集/.test(w))).toBe(true);
312
+ });
313
+
314
+ it("WeChat ≥ 8 + no root → blocked with root requirement", () => {
315
+ const r = interpretWechatProbe({
316
+ device: { reachable: true },
317
+ wechat: { installed: true, majorVersion: 8, versionName: "8.0.45" },
318
+ root: { detected: false },
319
+ frida: { serverRunning: false },
320
+ suggestedKeyProvider: "frida",
321
+ });
322
+ expect(r.readiness).toBe("blocked");
323
+ expect(r.blockers.join(" ")).toMatch(/root/);
324
+ expect(r.nextSteps.join(" ")).toMatch(/Magisk/);
325
+ });
326
+
327
+ it("WeChat ≥ 8 + root + no frida → partial with setup pointer", () => {
328
+ const r = interpretWechatProbe({
329
+ device: { reachable: true },
330
+ wechat: { installed: true, majorVersion: 8, versionName: "8.0.45" },
331
+ root: { detected: true },
332
+ frida: { serverRunning: false },
333
+ suggestedKeyProvider: "frida",
334
+ });
335
+ expect(r.readiness).toBe("partial");
336
+ expect(r.blockers.join(" ")).toMatch(/Frida server/i);
337
+ expect(r.nextSteps.join(" ")).toMatch(/Frida_Setup/);
338
+ });
339
+
340
+ it("WeChat ≥ 8 + root + frida running → ready", () => {
341
+ const r = interpretWechatProbe({
342
+ device: { reachable: true },
343
+ wechat: { installed: true, majorVersion: 8, versionName: "8.0.45" },
344
+ root: { detected: true },
345
+ frida: { serverRunning: true, port: 27042 },
346
+ suggestedKeyProvider: "frida",
347
+ });
348
+ expect(r.readiness).toBe("ready");
349
+ expect(r.nextSteps.some((s) => /register/.test(s))).toBe(true);
350
+ expect(r.nextSteps.some((s) => /前台运行/.test(s))).toBe(true);
351
+ });
352
+
353
+ it("env-probe judges unsupported → blocked with reasons", () => {
354
+ const r = interpretWechatProbe({
355
+ device: { reachable: true },
356
+ wechat: { installed: true, majorVersion: 8, versionName: "8.0.50" },
357
+ root: { detected: true },
358
+ frida: { serverRunning: true },
359
+ suggestedKeyProvider: "unsupported",
360
+ reasons: [
361
+ "MMKV-only storage on this build — neither MD5 nor Frida path works",
362
+ ],
363
+ });
364
+ expect(r.readiness).toBe("blocked");
365
+ expect(r.blockers.join(" ")).toMatch(/MMKV-only/);
366
+ });
367
+ });
368
+
369
+ describe("cc hub wechat doctor", () => {
370
+ it("--json includes probe + doctor.readiness/blockers/nextSteps", async () => {
371
+ const hub = fakeHub();
372
+ await _internal.cmdWechatDoctor({ _getHub: async () => hub, json: true });
373
+ const out = logSpy.mock.calls.map((c) => c[0]).join("\n");
374
+ const parsed = JSON.parse(out);
375
+ expect(parsed.probe).toEqual(
376
+ expect.objectContaining({ suggestedKeyProvider: "md5" }),
377
+ );
378
+ expect(parsed.doctor).toEqual(
379
+ expect.objectContaining({
380
+ readiness: "ready",
381
+ nextSteps: expect.any(Array),
382
+ }),
383
+ );
384
+ });
385
+
386
+ it("human-readable surfaces trap table reference + telemetry capture cmd", async () => {
387
+ const hub = fakeHub();
388
+ await _internal.cmdWechatDoctor({ _getHub: async () => hub });
389
+ const out = logSpy.mock.calls.map((c) => c.join(" ")).join("\n");
390
+ expect(out).toMatch(/Doctor:/);
391
+ expect(out).toMatch(/READY|PARTIAL|BLOCKED/);
392
+ expect(out).toMatch(/fridaTelemetry/);
393
+ expect(out).toMatch(/trap table|§5\.1|Runbook/i);
394
+ });
395
+
396
+ it("device unreachable: omits telemetry capture section (no point if blocked)", async () => {
397
+ const hub = fakeHub({
398
+ probe: {
399
+ ok: false,
400
+ suggestedKeyProvider: "unsupported",
401
+ device: { reachable: false },
402
+ wechat: {},
403
+ root: {},
404
+ frida: {},
405
+ reasons: [],
406
+ warnings: [],
407
+ },
408
+ });
409
+ await _internal.cmdWechatDoctor({ _getHub: async () => hub });
410
+ const out = logSpy.mock.calls.map((c) => c.join(" ")).join("\n");
411
+ expect(out).toMatch(/BLOCKED/);
412
+ expect(out).not.toMatch(/fridaTelemetry/);
242
413
  });
243
414
  });
@@ -499,8 +499,13 @@ async function cmdAIChatHealth(options) {
499
499
  const factoryDeps = options._factoryDeps || {};
500
500
  const hubDir =
501
501
  factoryDeps.hubDir || (await (options._getHub || getHub)()).hubDir;
502
- const { createAIChatHealthChecker } =
503
- await import("@chainlesschain/personal-data-hub/adapters/ai-chat-history/health-checker");
502
+ // Bypass vite import-analysis (which can't resolve subpath exports
503
+ // for dynamic imports in vitest SSR mode) by composing the specifier
504
+ // at runtime — the static analyzer skips non-literal arguments.
505
+ const hcSpecifier =
506
+ "@chainlesschain/personal-data-hub" +
507
+ "/adapters/ai-chat-history/health-checker";
508
+ const { createAIChatHealthChecker } = await import(hcSpecifier);
504
509
  const { createAccountsStore, createVendorAdapterBridge } =
505
510
  await import("../lib/personal-data-hub-aichat-wizard.js");
506
511
  const accountsStore =
@@ -653,6 +658,193 @@ async function cmdWechatList(options) {
653
658
  }
654
659
  }
655
660
 
661
+ /**
662
+ * `cc hub wechat doctor` — env-probe + actionable interpretation +
663
+ * inline reference to the Phase 12.9 §5.1 Frida hook trap table.
664
+ *
665
+ * Designed to be the single command user runs on a rooted Android during
666
+ * Phase 12.9 real-device E2E to figure out "what should I do next?" —
667
+ * combines env-probe output + readiness checklist (per md5 vs frida
668
+ * path) + post-register telemetry fields to capture if hook fails.
669
+ *
670
+ * Returns the same JSON as env-probe + a `doctor` block under --json so
671
+ * scripts can branch on `doctor.readiness === 'ready' | 'blocked' | 'partial'`.
672
+ */
673
+ async function cmdWechatDoctor(options) {
674
+ try {
675
+ const hub = await (options._getHub || getHub)();
676
+ const probe = await hub.probeWechatEnv();
677
+
678
+ // Determine readiness + concrete next-action based on probe shape.
679
+ const advice = interpretWechatProbe(probe);
680
+
681
+ if (options.json) {
682
+ printJson({ probe, doctor: advice });
683
+ return;
684
+ }
685
+
686
+ // Human-readable: re-use env-probe formatting then append advice.
687
+ logger.log(chalk.bold("WeChat env-probe:"));
688
+ logger.log(
689
+ ` ${probe.ok ? chalk.green("✓") : chalk.red("✗")} suggested: ${chalk.cyan(probe.suggestedKeyProvider)}`,
690
+ );
691
+ logger.log(
692
+ ` device: ${probe.device.reachable ? chalk.green("reachable") : chalk.red("unreachable")}${probe.device.serial ? " (" + probe.device.serial + ")" : ""} abi=${probe.device.abi || "?"}`,
693
+ );
694
+ logger.log(
695
+ ` root: ${probe.root.detected ? chalk.green("yes") : chalk.gray("no")} magisk=${probe.root.magiskInstalled ? "yes" : "no"}`,
696
+ );
697
+ logger.log(
698
+ ` frida-server: ${probe.frida.serverRunning ? chalk.green("running") : chalk.gray("not running")}${probe.frida.port ? " :" + probe.frida.port : ""}`,
699
+ );
700
+ logger.log(
701
+ ` wechat: ${probe.wechat.installed ? chalk.green(probe.wechat.versionName) : chalk.gray("not installed")}`,
702
+ );
703
+
704
+ logger.log("");
705
+ const statusColor =
706
+ advice.readiness === "ready"
707
+ ? chalk.green
708
+ : advice.readiness === "partial"
709
+ ? chalk.yellow
710
+ : chalk.red;
711
+ logger.log(
712
+ chalk.bold(`Doctor: ${statusColor(advice.readiness.toUpperCase())}`),
713
+ );
714
+ for (const blocker of advice.blockers) {
715
+ logger.log(` ${chalk.red("✗")} ${blocker}`);
716
+ }
717
+ for (const w of advice.warnings) {
718
+ logger.log(` ${chalk.yellow("!")} ${w}`);
719
+ }
720
+ for (const step of advice.nextSteps) {
721
+ logger.log(` ${chalk.cyan("→")} ${step}`);
722
+ }
723
+
724
+ if (advice.readiness !== "blocked") {
725
+ logger.log("");
726
+ logger.log(
727
+ chalk.bold("After `cc hub wechat register`, capture telemetry:"),
728
+ );
729
+ logger.log(
730
+ chalk.gray(
731
+ " cc hub wechat register --uin <UIN> --db ... --json | jq '.fridaTelemetry'",
732
+ ),
733
+ );
734
+ logger.log(
735
+ chalk.gray(
736
+ " Expected fields: hooked / keySource / keySig / keyFormat / keyLength / keyAlt / errors / durationMs",
737
+ ),
738
+ );
739
+ logger.log("");
740
+ logger.log(
741
+ chalk.bold("If hook fails, match telemetry against trap table:"),
742
+ );
743
+ logger.log(
744
+ chalk.gray(
745
+ " A — hooked:[] empty → libWCDB.so module name (try OEM custom names)",
746
+ ),
747
+ );
748
+ logger.log(
749
+ chalk.gray(
750
+ " B — keySig:v2 but DB won't open → sqlite3_key_v2 args index wrong",
751
+ ),
752
+ );
753
+ logger.log(
754
+ chalk.gray(
755
+ " C — keyFormat:raw-bytes but len=64 → ascii-hex path missed",
756
+ ),
757
+ );
758
+ logger.log(
759
+ chalk.gray(
760
+ " Full table: docs/design/Personal_Data_Hub_Phase_12_9_*Runbook.md §5.1",
761
+ ),
762
+ );
763
+ }
764
+ } catch (err) {
765
+ fail(null, err, options.json);
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Interpret env-probe result into { readiness, blockers, warnings, nextSteps }.
771
+ * Pure function — testable without device.
772
+ *
773
+ * @param {object} probe output of hub.probeWechatEnv()
774
+ * @returns {{readiness: 'ready'|'partial'|'blocked', blockers: string[],
775
+ * warnings: string[], nextSteps: string[]}}
776
+ */
777
+ function interpretWechatProbe(probe) {
778
+ const blockers = [];
779
+ const warnings = [];
780
+ const nextSteps = [];
781
+
782
+ if (!probe || !probe.device || !probe.device.reachable) {
783
+ blockers.push("adb 设备未连接 (USB 调试 / drivers / authorize?)");
784
+ nextSteps.push("`adb devices` 应列出至少一台 device 状态");
785
+ return { readiness: "blocked", blockers, warnings, nextSteps };
786
+ }
787
+
788
+ if (!probe.wechat || !probe.wechat.installed) {
789
+ blockers.push("WeChat (com.tencent.mm) 未安装");
790
+ nextSteps.push("先把 WeChat 装上 + 登一次产生 EnMicroMsg.db");
791
+ return { readiness: "blocked", blockers, warnings, nextSteps };
792
+ }
793
+
794
+ const ver = probe.wechat.majorVersion || 0;
795
+ const suggested = probe.suggestedKeyProvider;
796
+
797
+ if (ver < 8 && suggested === "md5") {
798
+ nextSteps.push(
799
+ "MD5 path: `adb pull /data/data/com.tencent.mm/ /tmp/wechat-data/`",
800
+ );
801
+ nextSteps.push(
802
+ "拉到本地后跑: `cc hub wechat register --uin <UIN> --db /tmp/wechat-data/MicroMsg/<md5-uin>/EnMicroMsg.db --wechat-data-path /tmp/wechat-data/`",
803
+ );
804
+ if (!probe.root.detected) {
805
+ warnings.push(
806
+ "非 root 设备只能 adb pull WeChat backup 子集 — 可能拿不到全部 db / 文件",
807
+ );
808
+ }
809
+ return { readiness: "ready", blockers, warnings, nextSteps };
810
+ }
811
+
812
+ if (ver >= 8 && suggested === "frida") {
813
+ if (!probe.root.detected) {
814
+ blockers.push("WeChat ≥ 8.0 必须 root + Frida hook,当前设备未 root");
815
+ nextSteps.push("Magisk 刷入 → 重启 → 重跑 doctor");
816
+ return { readiness: "blocked", blockers, warnings, nextSteps };
817
+ }
818
+ if (!probe.frida.serverRunning) {
819
+ blockers.push("Frida server 未运行");
820
+ nextSteps.push(
821
+ "见 docs/design/Adapter_WeChat_SQLCipher_Frida_Setup.md §2 启 frida-server",
822
+ );
823
+ return { readiness: "partial", blockers, warnings, nextSteps };
824
+ }
825
+ nextSteps.push(
826
+ "Frida path 就绪。register: `cc hub wechat register --uin <wxid>`(无需 --db / --wechat-data-path)",
827
+ );
828
+ nextSteps.push("WeChat 必须前台运行(已登录),register 期间不要切走");
829
+ return { readiness: "ready", blockers, warnings, nextSteps };
830
+ }
831
+
832
+ if (suggested === "unsupported") {
833
+ blockers.push("env-probe 判定为 unsupported");
834
+ for (const reason of probe.reasons || []) blockers.push(reason);
835
+ nextSteps.push(
836
+ "见 docs/design/Adapter_WeChat_SQLCipher.md §13 — 检查 WeChat 版本兼容矩阵",
837
+ );
838
+ return { readiness: "blocked", blockers, warnings, nextSteps };
839
+ }
840
+
841
+ // Fallback — probe shape we don't recognize
842
+ warnings.push(
843
+ `未识别的 suggested='${suggested}' (version=${probe.wechat.versionName || "?"})`,
844
+ );
845
+ return { readiness: "partial", blockers, warnings, nextSteps };
846
+ }
847
+
656
848
  async function cmdWechatUnregister(uin, options) {
657
849
  try {
658
850
  if (!uin) throw new Error("uin argument required");
@@ -901,6 +1093,16 @@ export function registerHubCommand(program) {
901
1093
  )
902
1094
  .option("--json", "Output JSON")
903
1095
  .action(cmdWechatUnregister);
1096
+
1097
+ // Phase 12.9 — diagnostic helper for real-device E2E. Combines env-probe
1098
+ // with actionable readiness checklist + inline §5.1 Frida trap reference.
1099
+ wechat
1100
+ .command("doctor")
1101
+ .description(
1102
+ "Diagnose WeChat setup — env-probe + readiness checklist + Phase 12.9 trap reference",
1103
+ )
1104
+ .option("--json", "Output JSON")
1105
+ .action(cmdWechatDoctor);
904
1106
  }
905
1107
 
906
1108
  // exported for tests — handler functions can be invoked directly with
@@ -917,5 +1119,7 @@ export const _internal = {
917
1119
  cmdWechatRegister,
918
1120
  cmdWechatList,
919
1121
  cmdWechatUnregister,
1122
+ cmdWechatDoctor,
1123
+ interpretWechatProbe,
920
1124
  _defaultKnownVendors,
921
1125
  };