chainlesschain 0.162.14 → 0.162.16

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 (142) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-D34d_Nh1.js → AIOps-6yP2WySy.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Br7HxCnl.js → ActionButton-BvpIMxiU.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-bVKq79Xd.js → Analytics-Bvg88oOa.js} +1 -1
  5. package/src/assets/web-panel/assets/{AppLayout-CWSLIbAz.js → AppLayout-Do30UWbY.js} +2 -2
  6. package/src/assets/web-panel/assets/{Audit-Cmnu1qqa.js → Audit-Bfshrs5j.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-Rok20-TL.js → Backup-yKQbHMJr.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-BJzs_ZtT.js → BaseInput-CxpV1ZXM.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-CSYapbcq.js → Chat-y1SfGDl1.js} +1 -1
  10. package/src/assets/web-panel/assets/{Checkbox-BEa7Sr7e.js → Checkbox-Dpoth43k.js} +1 -1
  11. package/src/assets/web-panel/assets/{Codegen-C9M4e7ne.js → Codegen-DbwH-QDz.js} +1 -1
  12. package/src/assets/web-panel/assets/{Col-DU9NoUIi.js → Col-B-MGUxt2.js} +1 -1
  13. package/src/assets/web-panel/assets/{Community-DA9uz_jP.js → Community-B521TAcS.js} +1 -1
  14. package/src/assets/web-panel/assets/{Compact-3_bEraVw.js → Compact-CislP3fF.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compliance-BtF8jWUQ.js → Compliance-BXqrD-tO.js} +1 -1
  16. package/src/assets/web-panel/assets/{Cowork-BqvA7oaM.js → Cowork-DtRMjAz_.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cron-CxZy7Mzg.js → Cron-CX8dYcdW.js} +1 -1
  18. package/src/assets/web-panel/assets/{Crosschain-1DB-XRGu.js → Crosschain-DeEwabS0.js} +1 -1
  19. package/src/assets/web-panel/assets/{DID-B6Ezp1pt.js → DID-DjDE7pgB.js} +1 -1
  20. package/src/assets/web-panel/assets/{Dashboard-QDJ6VVsn.js → Dashboard-pr7dCgLq.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dropdown-CovsWjxG.js → Dropdown-DuDw9Nfe.js} +1 -1
  22. package/src/assets/web-panel/assets/{Federation-DbRxS4Y4.js → Federation-BiDyaRHe.js} +1 -1
  23. package/src/assets/web-panel/assets/{FormItemContext-9E9dNGtx.js → FormItemContext-BEF_PW-L.js} +1 -1
  24. package/src/assets/web-panel/assets/{Git-CqEpyxRZ.js → Git-DIbvoylw.js} +1 -1
  25. package/src/assets/web-panel/assets/{Governance-On47KtGq.js → Governance-FFY2QV_a.js} +1 -1
  26. package/src/assets/web-panel/assets/{Inference-RZcjcyaq.js → Inference-Clgxm2a1.js} +1 -1
  27. package/src/assets/web-panel/assets/{KnowledgeGraph-C-1rRAM9.js → KnowledgeGraph-CNmtI1QB.js} +1 -1
  28. package/src/assets/web-panel/assets/{Logs-BuunmG_r.js → Logs-ibszFmVA.js} +1 -1
  29. package/src/assets/web-panel/assets/{Marketplace-CromymyA.js → Marketplace-CeBhDY6f.js} +1 -1
  30. package/src/assets/web-panel/assets/{McpTools-5XlFExh7.js → McpTools-Bl4misFt.js} +1 -1
  31. package/src/assets/web-panel/assets/{Memory-DjnUT7YM.js → Memory-CTGi-Zcb.js} +1 -1
  32. package/src/assets/web-panel/assets/{MobileBridge-BrYIgLg6.js → MobileBridge-F7jJrt63.js} +1 -1
  33. package/src/assets/web-panel/assets/{MobileProjects-CL5V3fTm.js → MobileProjects--p2v4WUQ.js} +1 -1
  34. package/src/assets/web-panel/assets/{Mtc-CHYJq6zK.js → Mtc-Cw0DfWPd.js} +1 -1
  35. package/src/assets/web-panel/assets/{MtcAudit-BZxUO0qt.js → MtcAudit-BNAv66VX.js} +1 -1
  36. package/src/assets/web-panel/assets/{Multisig-FZTmJgW1.js → Multisig-B4IxV-6K.js} +1 -1
  37. package/src/assets/web-panel/assets/{NLProgramming-C9Mhefph.js → NLProgramming-J5Nfy4jx.js} +1 -1
  38. package/src/assets/web-panel/assets/{Notes-W7usj-Ar.js → Notes-BQl5wafk.js} +1 -1
  39. package/src/assets/web-panel/assets/{NotificationSettings-PBuYv_Bh.js → NotificationSettings-1Sur2hZk.js} +1 -1
  40. package/src/assets/web-panel/assets/{Organization-CuYCE-rF.js → Organization-DSJGSd5V.js} +1 -1
  41. package/src/assets/web-panel/assets/{Overflow-Dojx-kzE.js → Overflow-6TgFQZGw.js} +1 -1
  42. package/src/assets/web-panel/assets/{P2P-BgIaSrLX.js → P2P-Cb-eWAUF.js} +1 -1
  43. package/src/assets/web-panel/assets/{Permissions-Byj2dkF_.js → Permissions-C_xekWqT.js} +1 -1
  44. package/src/assets/web-panel/assets/{PersonalDataHub-CMOOI13-.js → PersonalDataHub-C6X0ilxN.js} +1 -1
  45. package/src/assets/web-panel/assets/{Pipeline-CWwEOF09.js → Pipeline-CQlD8pLe.js} +1 -1
  46. package/src/assets/web-panel/assets/{Privacy-VT7gldcN.js → Privacy-DlTQcdGg.js} +1 -1
  47. package/src/assets/web-panel/assets/{ProjectInit-7UH3c3p7.js → ProjectInit-BicKKZtR.js} +1 -1
  48. package/src/assets/web-panel/assets/{ProjectSettings-DqLp-72a.js → ProjectSettings-CXTbWgIg.js} +1 -1
  49. package/src/assets/web-panel/assets/{Projects-B_54eDhH.js → Projects-Diq_IZJO.js} +1 -1
  50. package/src/assets/web-panel/assets/{Providers-BIrNfNpc.js → Providers-CKXjLBNU.js} +1 -1
  51. package/src/assets/web-panel/assets/{QuickAsk-BbYPwCso.js → QuickAsk-BnUavy-a.js} +1 -1
  52. package/src/assets/web-panel/assets/{Recommend-BF4qBssF.js → Recommend-qbDO-M0M.js} +1 -1
  53. package/src/assets/web-panel/assets/{Reputation-DPEzlC2V.js → Reputation-BnkI4I_D.js} +1 -1
  54. package/src/assets/web-panel/assets/{Row-DjHxhH1L.js → Row-sVpTpVTK.js} +1 -1
  55. package/src/assets/web-panel/assets/{RssFeed-D0_j678P.js → RssFeed-CN85L1Ph.js} +1 -1
  56. package/src/assets/web-panel/assets/{Search-DctfGehu.js → Search-Dxxlr3ob.js} +1 -1
  57. package/src/assets/web-panel/assets/{Security-BFHggeYM.js → Security-CVjWa1ad.js} +1 -1
  58. package/src/assets/web-panel/assets/{Services-CmrFMukV.js → Services-BPiPhH6q.js} +1 -1
  59. package/src/assets/web-panel/assets/{Skeleton-DR4vn_nS.js → Skeleton-BpJEB3Px.js} +1 -1
  60. package/src/assets/web-panel/assets/{Skills-DlXG2yyV.js → Skills-Cjiy1gsC.js} +1 -1
  61. package/src/assets/web-panel/assets/{Sla-4PPGL3SE.js → Sla-CW2YJRZw.js} +1 -1
  62. package/src/assets/web-panel/assets/{SpeechSettings-D9EhJOqm.js → SpeechSettings-DBGDc9cz.js} +1 -1
  63. package/src/assets/web-panel/assets/{SyncSettings-Dasmbi0p.js → SyncSettings-CWiQLmKL.js} +1 -1
  64. package/src/assets/web-panel/assets/{Tasks-vilEiuPA.js → Tasks-CVsvO2yt.js} +1 -1
  65. package/src/assets/web-panel/assets/{Templates-Ca9Rvktn.js → Templates-BEXmGilK.js} +1 -1
  66. package/src/assets/web-panel/assets/{Tenant-CEZb9gfK.js → Tenant-Dd_xNf2O.js} +1 -1
  67. package/src/assets/web-panel/assets/{Terminal-DanCBdbD.js → Terminal-JIkt-KLk.js} +1 -1
  68. package/src/assets/web-panel/assets/{Tokens-SPkClW2d.js → Tokens-Ca8aA-4X.js} +1 -1
  69. package/src/assets/web-panel/assets/{Trigger-B645yL7g.js → Trigger-CLDtLBuR.js} +1 -1
  70. package/src/assets/web-panel/assets/{Trust-D9sM_Ig0.js → Trust-DSzmH0eN.js} +1 -1
  71. package/src/assets/web-panel/assets/{UkeySign-B_Nr2K-u.js → UkeySign-D7yOYajn.js} +1 -1
  72. package/src/assets/web-panel/assets/{VideoEditing-U01Lea8j.js → VideoEditing-Dk3jUxyG.js} +1 -1
  73. package/src/assets/web-panel/assets/{Wallet-6xBySVV8.js → Wallet-BSB2tO_C.js} +1 -1
  74. package/src/assets/web-panel/assets/{WebAuthn-DbgMoBu6.js → WebAuthn-DAPZsZNw.js} +1 -1
  75. package/src/assets/web-panel/assets/{WorkflowEditor-Bz-Y6IR2.js → WorkflowEditor-DinHN1vM.js} +1 -1
  76. package/src/assets/web-panel/assets/{chat-BC_O9hag.js → chat-BLyN3kuS.js} +1 -1
  77. package/src/assets/web-panel/assets/{colors-ChlOGOvr.js → colors-B-IfN2x7.js} +1 -1
  78. package/src/assets/web-panel/assets/{compact-item-BSbAYGGF.js → compact-item-tVJNTa_f.js} +1 -1
  79. package/src/assets/web-panel/assets/{createContext-CFcZly5M.js → createContext-CeHSYhwo.js} +1 -1
  80. package/src/assets/web-panel/assets/{hasIn-BomYwwYE.js → hasIn-CP8akQuY.js} +1 -1
  81. package/src/assets/web-panel/assets/{index-QD_n54XT.js → index-3T9u2Hlb.js} +1 -1
  82. package/src/assets/web-panel/assets/{index-ozVPr1gj.js → index-B6RV21Hn.js} +1 -1
  83. package/src/assets/web-panel/assets/{index-CnxlKTDK.js → index-BAhYPqmm.js} +1 -1
  84. package/src/assets/web-panel/assets/{index-DNkth8dM.js → index-BDS9QmmD.js} +1 -1
  85. package/src/assets/web-panel/assets/{index-CoF95pYK.js → index-BRH7hf5q.js} +1 -1
  86. package/src/assets/web-panel/assets/{index-DsNQ2hqI.js → index-BZhvUfFb.js} +1 -1
  87. package/src/assets/web-panel/assets/{index-Ctx97mH-.js → index-B_pAhN3p.js} +1 -1
  88. package/src/assets/web-panel/assets/{index-BHGsFwYW.js → index-BsMpKD71.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-T5Y_9IPv.js → index-ByqjsQPb.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-D_0B3CiU.js → index-C44pVyK1.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BwFykZ5U.js → index-C6NyhfFI.js} +1 -1
  92. package/src/assets/web-panel/assets/index-C6dKiBgm.js +1 -0
  93. package/src/assets/web-panel/assets/{index-EY733h9z.js → index-COqTEK1x.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B6LJHQoE.js → index-CP5ax2Ox.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BZGdjNLA.js → index-CPdpqzTz.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DW-Ji07y.js → index-C_sA6V0-.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BEJ6YiLI.js → index-C_we-zLv.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-5Ewm6KZA.js → index-Cjg_lXkn.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-C0rr1X9W.js → index-CkMJbSJn.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BI1jAWcc.js → index-CmFox476.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-Cmzh8gKL.js → index-CrxfAG6t.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BHi69MHF.js → index-CtgPPk47.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D0vX9jQA.js → index-CxRjf-uA.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CCRSz2cR.js → index-D0Lw2-GE.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-za1GUJBG.js → index-D338fQ78.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-slYX2rCE.js → index-D7xlQtKS.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CBhoZhCO.js → index-DISTBJJ4.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-b8GbH2Yi.js → index-DL2e2Kwh.js} +3 -3
  109. package/src/assets/web-panel/assets/{index-DXgE2VW6.js → index-DNpXi3KX.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DRp5_Xns.js → index-DVbSr5gk.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BYDvb1pi.js → index-DYEooXtQ.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BGUbtM3R.js → index-DaXMEgmQ.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Dbf5YmDX.js → index-DfzQT6CX.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-gUACAWbM.js → index-Dk_cKbpp.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-ByZQNO0A.js → index-pO_TKKcK.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-onW325hZ.js → index-q4cwusSB.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-t9u2bHpH.js → index-rdeSvPj9.js} +1 -1
  118. package/src/assets/web-panel/assets/index-vl01xWcc.js +1 -0
  119. package/src/assets/web-panel/assets/{index-Cj47XwJQ.js → index-xaqrvGuG.js} +1 -1
  120. package/src/assets/web-panel/assets/{initDefaultProps-DnadEaxu.js → initDefaultProps-Cw37W7y2.js} +1 -1
  121. package/src/assets/web-panel/assets/{motion-CC_Na0Tl.js → motion-DOWpjqMY.js} +1 -1
  122. package/src/assets/web-panel/assets/{move-C2d9Mkk9.js → move-Ha0Ha2IK.js} +1 -1
  123. package/src/assets/web-panel/assets/{omit-QvpKbF8p.js → omit-DwGrzMoG.js} +1 -1
  124. package/src/assets/web-panel/assets/{pickAttrs-Dm8r3X1_.js → pickAttrs-BRVWfNub.js} +1 -1
  125. package/src/assets/web-panel/assets/{placementArrow-DaqaVfoX.js → placementArrow-hV_RKr6c.js} +1 -1
  126. package/src/assets/web-panel/assets/{responsiveObserve-Iida9fIn.js → responsiveObserve-PWGubgde.js} +1 -1
  127. package/src/assets/web-panel/assets/{slide-YqHexXQD.js → slide-TRaJdEGF.js} +1 -1
  128. package/src/assets/web-panel/assets/{statusUtils-BGKLoeEt.js → statusUtils-Dtpf-U70.js} +1 -1
  129. package/src/assets/web-panel/assets/{styleChecker-aI-gsQO8.js → styleChecker-pWt7CTDQ.js} +1 -1
  130. package/src/assets/web-panel/assets/{useFlexGapSupport-BiOsz4rc.js → useFlexGapSupport-BojArhOx.js} +1 -1
  131. package/src/assets/web-panel/assets/{useFs-CZy7Zo2X.js → useFs-ZGDRvFRC.js} +1 -1
  132. package/src/assets/web-panel/assets/{vnode-B6WqjmE4.js → vnode-CBtr3ooD.js} +1 -1
  133. package/src/assets/web-panel/assets/{zoom-DTeTrJ2z.js → zoom-DyGC413M.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 +368 -3
  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-aichat-wizard.js +29 -15
  140. package/src/lib/personal-data-hub-wiring.js +24 -2
  141. package/src/assets/web-panel/assets/index-BIRYt1of.js +0 -1
  142. 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
  });
@@ -43,9 +43,16 @@ async function cmdAsk(question, options) {
43
43
  try {
44
44
  const hub = await getHub();
45
45
  if (!hub.engine) throw new Error("Analysis engine unavailable");
46
+ // 推文 §三道锁第二把 "默认不许问云端" — acceptNonLocal 默认 false (拒云)。
47
+ // 优先 --accept-non-local CLI 旗,其次 env CC_HUB_ALLOW_NON_LOCAL (Android UI
48
+ // 拒云 toggle 通过 LocalCcRunner.askQuestion 透传),否则维持 false。
49
+ const envAllow =
50
+ process.env.CC_HUB_ALLOW_NON_LOCAL === "1" ||
51
+ process.env.CC_HUB_ALLOW_NON_LOCAL === "true";
52
+ const acceptNonLocal = !!options.acceptNonLocal || envAllow;
46
53
  const result = await hub.engine.ask(question, {
47
54
  useRag: options.useRag !== false,
48
- acceptNonLocal: !!options.acceptNonLocal,
55
+ acceptNonLocal,
49
56
  });
50
57
  if (spinner) spinner.stop();
51
58
  if (options.json) {
@@ -306,6 +313,140 @@ async function cmdRegisterMock(options) {
306
313
  }
307
314
  }
308
315
 
316
+ // ─── event-detail ────────────────────────────────────────────────────
317
+
318
+ /**
319
+ * `cc hub event-detail <eventId> [--json]`
320
+ *
321
+ * 推文 §"AI 回答必须给出处" 的兑现路径——点 citation chip → 调本命令 → 显
322
+ * 原文。Returns the full event row from the vault, including subtype /
323
+ * source / actor / title / timestamps / payload-derived fields.
324
+ *
325
+ * Caller is typically the Android UI (HubAskCard citation chip click) or
326
+ * desktop renderer. Returns `null` shape `{ found: false, eventId }` when
327
+ * the id doesn't match (e.g. event was deleted by destroy after the ask).
328
+ */
329
+ async function cmdEventDetail(eventId, options) {
330
+ if (!eventId) {
331
+ const msg = "eventId argument required";
332
+ if (options.json) printJson({ error: msg });
333
+ else logger.error(chalk.red(`✗ ${msg}`));
334
+ process.exit(1);
335
+ }
336
+ try {
337
+ const hub = await getHub();
338
+ const event = hub.vault.getEvent(eventId);
339
+ if (!event) {
340
+ const result = { found: false, eventId };
341
+ if (options.json) printJson(result);
342
+ else logger.log(chalk.yellow(`(no event with id ${eventId})`));
343
+ return;
344
+ }
345
+ if (options.json) {
346
+ printJson({ found: true, event });
347
+ } else {
348
+ logger.log(chalk.bold(`event ${event.id}`));
349
+ logger.log(` subtype: ${event.subtype}`);
350
+ logger.log(` source: ${event.source}`);
351
+ if (event.title) logger.log(` title: ${event.title}`);
352
+ if (event.actor) logger.log(` actor: ${event.actor}`);
353
+ if (event.amount != null) {
354
+ logger.log(` amount: ${event.amount} ${event.currency || ""}`);
355
+ }
356
+ if (event.startedAt) {
357
+ logger.log(` started: ${new Date(event.startedAt).toISOString()}`);
358
+ }
359
+ }
360
+ } catch (err) {
361
+ fail(null, err, options.json);
362
+ }
363
+ }
364
+
365
+ // ─── export ──────────────────────────────────────────────────────────
366
+
367
+ /**
368
+ * `cc hub export --output <path> [--json]`
369
+ *
370
+ * 推文 §"一键带走"。Closes the vault, copies vault.db (+ WAL/SHM if present)
371
+ * to the destination path, then reopens. Resulting file is SQLCipher-encrypted
372
+ * exactly as on disk — caller (Android UI) hands the bytes off via SAF
373
+ * picker. Desktop side can `cc hub import-vault <path>` to reimport.
374
+ *
375
+ * No re-encryption / re-keying. The file IS the export. Key handling is
376
+ * out of scope here — the file is useless without the user's keystore-backed
377
+ * key material (which is bound to device by KeyProvider).
378
+ */
379
+ async function cmdExport(options) {
380
+ if (!options.output) {
381
+ const msg = "--output <path> required";
382
+ if (options.json) printJson({ error: msg });
383
+ else logger.error(chalk.red(`✗ ${msg}`));
384
+ process.exit(1);
385
+ }
386
+ try {
387
+ const fs = await import("node:fs");
388
+ const path = await import("node:path");
389
+ const hub = await getHub();
390
+ const src = hub.vault.path;
391
+ if (!src) throw new Error("vault path unavailable (hub not initialized?)");
392
+
393
+ // Resolve output → absolute path. Make parent directory if missing.
394
+ const outAbs = path.resolve(options.output);
395
+ fs.mkdirSync(path.dirname(outAbs), { recursive: true });
396
+
397
+ // Close the vault so we copy a consistent snapshot. better-sqlite3 keeps
398
+ // WAL pages buffered; closing flushes everything to disk. We reopen at
399
+ // end so the running hub session continues to work.
400
+ try {
401
+ hub.vault.close?.();
402
+ } catch (_e) {
403
+ // ignore — closing failure is rare and we still try to copy
404
+ }
405
+
406
+ // Copy main db file. WAL / SHM are sidecars — only copy if present (they
407
+ // may or may not exist depending on WAL mode + last checkpoint).
408
+ let bytes = 0;
409
+ fs.copyFileSync(src, outAbs);
410
+ bytes += fs.statSync(outAbs).size;
411
+ for (const suffix of ["-wal", "-shm"]) {
412
+ const sidecar = src + suffix;
413
+ if (fs.existsSync(sidecar)) {
414
+ const outSidecar = outAbs + suffix;
415
+ fs.copyFileSync(sidecar, outSidecar);
416
+ bytes += fs.statSync(outSidecar).size;
417
+ }
418
+ }
419
+
420
+ // Reopen so the same hub instance can keep serving.
421
+ try {
422
+ hub.vault.open?.();
423
+ } catch (e) {
424
+ // If reopen fails the next operation will surface it; don't block export.
425
+ logger.warn?.(
426
+ chalk.yellow(
427
+ `! vault.open after export failed: ${e?.message || e} — restart cc`,
428
+ ),
429
+ );
430
+ }
431
+
432
+ const result = {
433
+ ok: true,
434
+ source: src,
435
+ output: outAbs,
436
+ bytes,
437
+ // Hint to caller (e.g. Android UI) that this is encrypted at rest.
438
+ encrypted: true,
439
+ };
440
+ if (options.json) {
441
+ printJson(result);
442
+ } else {
443
+ logger.log(chalk.green(`✓ exported ${bytes} bytes to ${outAbs}`));
444
+ }
445
+ } catch (err) {
446
+ fail(null, err, options.json);
447
+ }
448
+ }
449
+
309
450
  // ─── destroy ─────────────────────────────────────────────────────────
310
451
 
311
452
  async function cmdDestroy(options) {
@@ -499,8 +640,13 @@ async function cmdAIChatHealth(options) {
499
640
  const factoryDeps = options._factoryDeps || {};
500
641
  const hubDir =
501
642
  factoryDeps.hubDir || (await (options._getHub || getHub)()).hubDir;
502
- const { createAIChatHealthChecker } =
503
- await import("@chainlesschain/personal-data-hub/adapters/ai-chat-history/health-checker");
643
+ // Bypass vite import-analysis (which can't resolve subpath exports
644
+ // for dynamic imports in vitest SSR mode) by composing the specifier
645
+ // at runtime — the static analyzer skips non-literal arguments.
646
+ const hcSpecifier =
647
+ "@chainlesschain/personal-data-hub" +
648
+ "/adapters/ai-chat-history/health-checker";
649
+ const { createAIChatHealthChecker } = await import(hcSpecifier);
504
650
  const { createAccountsStore, createVendorAdapterBridge } =
505
651
  await import("../lib/personal-data-hub-aichat-wizard.js");
506
652
  const accountsStore =
@@ -653,6 +799,193 @@ async function cmdWechatList(options) {
653
799
  }
654
800
  }
655
801
 
802
+ /**
803
+ * `cc hub wechat doctor` — env-probe + actionable interpretation +
804
+ * inline reference to the Phase 12.9 §5.1 Frida hook trap table.
805
+ *
806
+ * Designed to be the single command user runs on a rooted Android during
807
+ * Phase 12.9 real-device E2E to figure out "what should I do next?" —
808
+ * combines env-probe output + readiness checklist (per md5 vs frida
809
+ * path) + post-register telemetry fields to capture if hook fails.
810
+ *
811
+ * Returns the same JSON as env-probe + a `doctor` block under --json so
812
+ * scripts can branch on `doctor.readiness === 'ready' | 'blocked' | 'partial'`.
813
+ */
814
+ async function cmdWechatDoctor(options) {
815
+ try {
816
+ const hub = await (options._getHub || getHub)();
817
+ const probe = await hub.probeWechatEnv();
818
+
819
+ // Determine readiness + concrete next-action based on probe shape.
820
+ const advice = interpretWechatProbe(probe);
821
+
822
+ if (options.json) {
823
+ printJson({ probe, doctor: advice });
824
+ return;
825
+ }
826
+
827
+ // Human-readable: re-use env-probe formatting then append advice.
828
+ logger.log(chalk.bold("WeChat env-probe:"));
829
+ logger.log(
830
+ ` ${probe.ok ? chalk.green("✓") : chalk.red("✗")} suggested: ${chalk.cyan(probe.suggestedKeyProvider)}`,
831
+ );
832
+ logger.log(
833
+ ` device: ${probe.device.reachable ? chalk.green("reachable") : chalk.red("unreachable")}${probe.device.serial ? " (" + probe.device.serial + ")" : ""} abi=${probe.device.abi || "?"}`,
834
+ );
835
+ logger.log(
836
+ ` root: ${probe.root.detected ? chalk.green("yes") : chalk.gray("no")} magisk=${probe.root.magiskInstalled ? "yes" : "no"}`,
837
+ );
838
+ logger.log(
839
+ ` frida-server: ${probe.frida.serverRunning ? chalk.green("running") : chalk.gray("not running")}${probe.frida.port ? " :" + probe.frida.port : ""}`,
840
+ );
841
+ logger.log(
842
+ ` wechat: ${probe.wechat.installed ? chalk.green(probe.wechat.versionName) : chalk.gray("not installed")}`,
843
+ );
844
+
845
+ logger.log("");
846
+ const statusColor =
847
+ advice.readiness === "ready"
848
+ ? chalk.green
849
+ : advice.readiness === "partial"
850
+ ? chalk.yellow
851
+ : chalk.red;
852
+ logger.log(
853
+ chalk.bold(`Doctor: ${statusColor(advice.readiness.toUpperCase())}`),
854
+ );
855
+ for (const blocker of advice.blockers) {
856
+ logger.log(` ${chalk.red("✗")} ${blocker}`);
857
+ }
858
+ for (const w of advice.warnings) {
859
+ logger.log(` ${chalk.yellow("!")} ${w}`);
860
+ }
861
+ for (const step of advice.nextSteps) {
862
+ logger.log(` ${chalk.cyan("→")} ${step}`);
863
+ }
864
+
865
+ if (advice.readiness !== "blocked") {
866
+ logger.log("");
867
+ logger.log(
868
+ chalk.bold("After `cc hub wechat register`, capture telemetry:"),
869
+ );
870
+ logger.log(
871
+ chalk.gray(
872
+ " cc hub wechat register --uin <UIN> --db ... --json | jq '.fridaTelemetry'",
873
+ ),
874
+ );
875
+ logger.log(
876
+ chalk.gray(
877
+ " Expected fields: hooked / keySource / keySig / keyFormat / keyLength / keyAlt / errors / durationMs",
878
+ ),
879
+ );
880
+ logger.log("");
881
+ logger.log(
882
+ chalk.bold("If hook fails, match telemetry against trap table:"),
883
+ );
884
+ logger.log(
885
+ chalk.gray(
886
+ " A — hooked:[] empty → libWCDB.so module name (try OEM custom names)",
887
+ ),
888
+ );
889
+ logger.log(
890
+ chalk.gray(
891
+ " B — keySig:v2 but DB won't open → sqlite3_key_v2 args index wrong",
892
+ ),
893
+ );
894
+ logger.log(
895
+ chalk.gray(
896
+ " C — keyFormat:raw-bytes but len=64 → ascii-hex path missed",
897
+ ),
898
+ );
899
+ logger.log(
900
+ chalk.gray(
901
+ " Full table: docs/design/Personal_Data_Hub_Phase_12_9_*Runbook.md §5.1",
902
+ ),
903
+ );
904
+ }
905
+ } catch (err) {
906
+ fail(null, err, options.json);
907
+ }
908
+ }
909
+
910
+ /**
911
+ * Interpret env-probe result into { readiness, blockers, warnings, nextSteps }.
912
+ * Pure function — testable without device.
913
+ *
914
+ * @param {object} probe output of hub.probeWechatEnv()
915
+ * @returns {{readiness: 'ready'|'partial'|'blocked', blockers: string[],
916
+ * warnings: string[], nextSteps: string[]}}
917
+ */
918
+ function interpretWechatProbe(probe) {
919
+ const blockers = [];
920
+ const warnings = [];
921
+ const nextSteps = [];
922
+
923
+ if (!probe || !probe.device || !probe.device.reachable) {
924
+ blockers.push("adb 设备未连接 (USB 调试 / drivers / authorize?)");
925
+ nextSteps.push("`adb devices` 应列出至少一台 device 状态");
926
+ return { readiness: "blocked", blockers, warnings, nextSteps };
927
+ }
928
+
929
+ if (!probe.wechat || !probe.wechat.installed) {
930
+ blockers.push("WeChat (com.tencent.mm) 未安装");
931
+ nextSteps.push("先把 WeChat 装上 + 登一次产生 EnMicroMsg.db");
932
+ return { readiness: "blocked", blockers, warnings, nextSteps };
933
+ }
934
+
935
+ const ver = probe.wechat.majorVersion || 0;
936
+ const suggested = probe.suggestedKeyProvider;
937
+
938
+ if (ver < 8 && suggested === "md5") {
939
+ nextSteps.push(
940
+ "MD5 path: `adb pull /data/data/com.tencent.mm/ /tmp/wechat-data/`",
941
+ );
942
+ nextSteps.push(
943
+ "拉到本地后跑: `cc hub wechat register --uin <UIN> --db /tmp/wechat-data/MicroMsg/<md5-uin>/EnMicroMsg.db --wechat-data-path /tmp/wechat-data/`",
944
+ );
945
+ if (!probe.root.detected) {
946
+ warnings.push(
947
+ "非 root 设备只能 adb pull WeChat backup 子集 — 可能拿不到全部 db / 文件",
948
+ );
949
+ }
950
+ return { readiness: "ready", blockers, warnings, nextSteps };
951
+ }
952
+
953
+ if (ver >= 8 && suggested === "frida") {
954
+ if (!probe.root.detected) {
955
+ blockers.push("WeChat ≥ 8.0 必须 root + Frida hook,当前设备未 root");
956
+ nextSteps.push("Magisk 刷入 → 重启 → 重跑 doctor");
957
+ return { readiness: "blocked", blockers, warnings, nextSteps };
958
+ }
959
+ if (!probe.frida.serverRunning) {
960
+ blockers.push("Frida server 未运行");
961
+ nextSteps.push(
962
+ "见 docs/design/Adapter_WeChat_SQLCipher_Frida_Setup.md §2 启 frida-server",
963
+ );
964
+ return { readiness: "partial", blockers, warnings, nextSteps };
965
+ }
966
+ nextSteps.push(
967
+ "Frida path 就绪。register: `cc hub wechat register --uin <wxid>`(无需 --db / --wechat-data-path)",
968
+ );
969
+ nextSteps.push("WeChat 必须前台运行(已登录),register 期间不要切走");
970
+ return { readiness: "ready", blockers, warnings, nextSteps };
971
+ }
972
+
973
+ if (suggested === "unsupported") {
974
+ blockers.push("env-probe 判定为 unsupported");
975
+ for (const reason of probe.reasons || []) blockers.push(reason);
976
+ nextSteps.push(
977
+ "见 docs/design/Adapter_WeChat_SQLCipher.md §13 — 检查 WeChat 版本兼容矩阵",
978
+ );
979
+ return { readiness: "blocked", blockers, warnings, nextSteps };
980
+ }
981
+
982
+ // Fallback — probe shape we don't recognize
983
+ warnings.push(
984
+ `未识别的 suggested='${suggested}' (version=${probe.wechat.versionName || "?"})`,
985
+ );
986
+ return { readiness: "partial", blockers, warnings, nextSteps };
987
+ }
988
+
656
989
  async function cmdWechatUnregister(uin, options) {
657
990
  try {
658
991
  if (!uin) throw new Error("uin argument required");
@@ -794,6 +1127,26 @@ export function registerHubCommand(program) {
794
1127
  .option("--json", "Output JSON")
795
1128
  .action(cmdRunSkill);
796
1129
 
1130
+ hub
1131
+ .command("event-detail <eventId>")
1132
+ .description(
1133
+ "推文 §AI 给出处: fetch full event row from local vault by id (used by citation chip deeplink).",
1134
+ )
1135
+ .option("--json", "Output JSON")
1136
+ .action(cmdEventDetail);
1137
+
1138
+ hub
1139
+ .command("export")
1140
+ .description(
1141
+ "推文 §一键带走: copy SQLCipher vault.db (+ WAL/SHM) to <path>. Encrypted at rest; reimport via cc hub import-vault on desktop.",
1142
+ )
1143
+ .requiredOption(
1144
+ "--output <path>",
1145
+ "Destination file path for the vault copy",
1146
+ )
1147
+ .option("--json", "Output JSON")
1148
+ .action(cmdExport);
1149
+
797
1150
  hub
798
1151
  .command("destroy")
799
1152
  .description(
@@ -901,6 +1254,16 @@ export function registerHubCommand(program) {
901
1254
  )
902
1255
  .option("--json", "Output JSON")
903
1256
  .action(cmdWechatUnregister);
1257
+
1258
+ // Phase 12.9 — diagnostic helper for real-device E2E. Combines env-probe
1259
+ // with actionable readiness checklist + inline §5.1 Frida trap reference.
1260
+ wechat
1261
+ .command("doctor")
1262
+ .description(
1263
+ "Diagnose WeChat setup — env-probe + readiness checklist + Phase 12.9 trap reference",
1264
+ )
1265
+ .option("--json", "Output JSON")
1266
+ .action(cmdWechatDoctor);
904
1267
  }
905
1268
 
906
1269
  // exported for tests — handler functions can be invoked directly with
@@ -917,5 +1280,7 @@ export const _internal = {
917
1280
  cmdWechatRegister,
918
1281
  cmdWechatList,
919
1282
  cmdWechatUnregister,
1283
+ cmdWechatDoctor,
1284
+ interpretWechatProbe,
920
1285
  _defaultKnownVendors,
921
1286
  };