chainlesschain 0.162.17 → 0.162.20
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.
- package/package.json +3 -3
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-BnOiVQsQ.js → AIOps-Buh_mYlF.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Tw6tLfY1.js → ActionButton-Dc7c0bZf.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Des59eT3.js → Analytics-CR9TWfM-.js} +2 -2
- package/src/assets/web-panel/assets/AppLayout-DVfanUep.js +3 -0
- package/src/assets/web-panel/assets/{AppLayout-BLCe1k8m.css → AppLayout-DleZFgpe.css} +1 -1
- package/src/assets/web-panel/assets/{Audit-JX1upUxG.js → Audit-BxojBqRe.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-ByvQLRhY.js → Backup-CGkreaK9.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DC-h8Hd-.js → BaseInput-B-Q2ZGfz.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-D9dJ3NKb.js → Chat-Dd_VC5Av.js} +3 -3
- package/src/assets/web-panel/assets/ChatBubbleRenderer-B64kZfRH.js +1 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-CO908iBt.css +1 -0
- package/src/assets/web-panel/assets/{Checkbox-n3tD-95B.js → Checkbox-NYBD1rCW.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BV9t0CmU.js → Codegen-B3Kqhgv1.js} +1 -1
- package/src/assets/web-panel/assets/{Col-4KhPnXFB.js → Col-2A3Bg2GC.js} +1 -1
- package/src/assets/web-panel/assets/{Community-R1hZqHEr.js → Community-C84SkDEJ.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Bo4r1Z3L.js → Compact-Crv3pyZJ.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Dwywk8U2.js → Compliance-D4qasxoY.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DTBwVdcR.js → Cowork-Bfbvysw-.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-BrveD2Al.js → Cron-Dkjyiekd.js} +1 -1
- package/src/assets/web-panel/assets/{Crosschain-C63oZCeg.js → Crosschain-DbJm9Kjd.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Cyfo7zh4.js → DID-C2HI6acG.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-_EOI1Ax4.js → Dashboard-BXMY7QVP.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BWYHGHO3.js → Dropdown-DcOykpfh.js} +1 -1
- package/src/assets/web-panel/assets/EmailListRenderer-CVJ1j3Ll.css +1 -0
- package/src/assets/web-panel/assets/EmailListRenderer-Doo52F2M.js +1 -0
- package/src/assets/web-panel/assets/{Federation-CU03AB2d.js → Federation-cHubZT6U.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DECACp9C.js → FormItemContext-B5OVlspi.js} +1 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-DtJFcJYl.css +1 -0
- package/src/assets/web-panel/assets/GenericCardRenderer-OgA46PJq.js +1 -0
- package/src/assets/web-panel/assets/{Git-BhpPR-ck.js → Git-_OKH_JOq.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BtpmvOHi.js → Governance-CTByBwSV.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DfPuEW02.js → Inference-C0nqgySA.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CeA_2NFe.js → KnowledgeGraph-CWW4ZIqH.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-C_ozF-bn.js → Logs-DrwqUltr.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-WgBvu6Ma.js → Marketplace-LJMRx-dm.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CNIz8liM.js → McpTools-F_EjC8j3.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-DEN8NgRg.js → Memory-VonDem9-.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CP0rj-Fx.js → MobileBridge-CUWxIrfb.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-x3zDbPDI.js → MobileProjects-CGOo70lk.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CC4PEFJV.js → Mtc-CqUquFbQ.js} +1 -1
- package/src/assets/web-panel/assets/{MtcAudit-BHB0GvyW.js → MtcAudit-CRVusaip.js} +1 -1
- package/src/assets/web-panel/assets/{Multisig-BUPfLbde.js → Multisig-B0o8Mv8T.js} +2 -2
- package/src/assets/web-panel/assets/{NLProgramming-DNOkByM8.js → NLProgramming-CxQINUhu.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-U-RIv-kE.js → Notes-B6Q_apFW.js} +1 -1
- package/src/assets/web-panel/assets/{NotificationSettings-B-q6tsxf.js → NotificationSettings-DO-lgp6x.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BnOISpKI.css +1 -0
- package/src/assets/web-panel/assets/OrderTableRenderer-DxoBoMtz.js +1 -0
- package/src/assets/web-panel/assets/{Organization-DJLNuZ2_.js → Organization-DqJ_KawH.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BxTl0aeE.js → Overflow-Cg4iKODM.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Czzalu_0.js → P2P-n5GPMrwC.js} +1 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-B9ZGFpn4.css +1 -0
- package/src/assets/web-panel/assets/PdhVaultBrowser-BdZI0EJs.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-BBw06EGK.js → Permissions-BjtwAIGb.js} +3 -3
- package/src/assets/web-panel/assets/PersonalDataHub-Bsf3Wh6n.js +1 -0
- package/src/assets/web-panel/assets/{PersonalDataHub-Dvaa8niQ.css → PersonalDataHub-D0ncF92t.css} +1 -1
- package/src/assets/web-panel/assets/{Pipeline-qLLWNe2i.js → Pipeline-BZe2-Flj.js} +1 -1
- package/src/assets/web-panel/assets/Privacy-OJqMon4c.js +1 -0
- package/src/assets/web-panel/assets/{ProjectInit-CmJ0VPGy.js → ProjectInit-Dw-5SMG1.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-D1Xm2EwO.js → ProjectSettings-DxqW44cV.js} +1 -1
- package/src/assets/web-panel/assets/{Projects-DLN-awqS.js → Projects-CPOqs2ec.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D164DGWo.js → Providers-hBfwscEl.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BRCH7WgH.js → QuickAsk-DA3WFFAr.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BWZhT_-2.js → Recommend-DMpz3URg.js} +1 -1
- package/src/assets/web-panel/assets/Reputation-6-z56FwC.js +1 -0
- package/src/assets/web-panel/assets/{Row-BbuTB-ny.js → Row-CL1Y6SfW.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CLegyrrb.js → RssFeed-BIHU2bMf.js} +3 -3
- package/src/assets/web-panel/assets/{Search-Bz41pkvW.js → Search-CRAfuEX-.js} +1 -1
- package/src/assets/web-panel/assets/{Security-D4NWRpgV.js → Security-CVya3gvH.js} +3 -3
- package/src/assets/web-panel/assets/{Services-CP_6SXST.js → Services-Cxv7_umF.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CdRO3MnV.js → Skeleton-D_GvsCtv.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BmghgIL3.js → Skills-DyiEpcvf.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DjAIXxkm.js → Sla-wiglQzXV.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DYk-En4R.js → SpeechSettings-CLtovaa5.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CglX98G4.js → SyncSettings-BOgVef1O.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-BcLMTSP3.js → Tasks-Ik8AX7-J.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BCs5bP8P.js → Templates-CireiINW.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-B5jLtdWR.js → Tenant-t7jF9NkQ.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CZKKYYaA.js → Terminal-Btq_O951.js} +1 -1
- package/src/assets/web-panel/assets/TimelineRenderer-DmztdsbF.css +1 -0
- package/src/assets/web-panel/assets/TimelineRenderer-hC_Piiwy.js +1 -0
- package/src/assets/web-panel/assets/{Tokens-B8aHonMu.js → Tokens-BTIqA2J1.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-D6jDjaX1.js → Trigger-Dzv2gJpK.js} +1 -1
- package/src/assets/web-panel/assets/Trust-B13QpWOh.js +1 -0
- package/src/assets/web-panel/assets/{UkeySign-B1IDXsz6.js → UkeySign-CFj3UF4G.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DHvglMzO.js → VideoEditing-CGMOGTQb.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DAUh8j_b.js → Wallet-BkCJ573Y.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-D0Y8wGZt.js → WebAuthn-C8dCrmGH.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-BqZgQo4U.js → WorkflowEditor-CclfhxwH.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CtfIYzWq.js → chat-CW_X_A9l.js} +1 -1
- package/src/assets/web-panel/assets/{colors-B6ZfKKgZ.js → colors-6GtheJb2.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item--HQOl9D1.js → compact-item-tz7dJapw.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Bf-YNOPI.js → createContext-CbiGcRCw.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-D5G7UiYx.js → hasIn-bmNFD3PH.js} +1 -1
- package/src/assets/web-panel/assets/icons-AILO5ZcK.js +57 -0
- package/src/assets/web-panel/assets/{index-DJlTZ7oK.js → index-B2P1c5yD.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZyNnZb9.js → index-B3UH6whC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkoQfJv0.js → index-B6GgMo1k.js} +1 -1
- package/src/assets/web-panel/assets/{index-ofgv0VHh.js → index-B9sJpsz3.js} +1 -1
- package/src/assets/web-panel/assets/index-BFkZAdio.js +1 -0
- package/src/assets/web-panel/assets/{index-CGG5O6Zh.js → index-BM4slfDH.js} +1 -1
- package/src/assets/web-panel/assets/index-BhgINPAH.js +1 -0
- package/src/assets/web-panel/assets/{index-UppuQXbk.js → index-BxCPkuTG.js} +2 -2
- package/src/assets/web-panel/assets/{index-BPvA_8ed.js → index-C5zlrlPd.js} +1 -1
- package/src/assets/web-panel/assets/{index-OSlN9c4x.js → index-CBNnbQxM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWOeC8v6.js → index-CKP-AZD3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIYydwQc.js → index-CLk3xgD2.js} +1 -1
- package/src/assets/web-panel/assets/{index-2QuOKVo1.js → index-CQ7aJ-8s.js} +1 -1
- package/src/assets/web-panel/assets/{index-_ZD6hPvh.js → index-CTnIkosg.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ca_5Za9D.js → index-CU0MeCwH.js} +1 -1
- package/src/assets/web-panel/assets/{index-B23hk7ZY.js → index-CV9aPPYM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtjPgDzA.js → index-CYIwY9zQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-XwovMqXr.js → index-Cb33JbDx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOzNKGtD.js → index-CgLF_zD1.js} +1 -1
- package/src/assets/web-panel/assets/{index-CihsW323.js → index-Cs5KWviO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAPj11l4.js → index-CzIQ0u4e.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzvrUpDp.js → index-DK7ee5hC.js} +1 -1
- package/src/assets/web-panel/assets/{index-g_d5tOWL.js → index-DMGJqZ-o.js} +1 -1
- package/src/assets/web-panel/assets/{index--KnB-1F0.js → index-Da2RUdcb.js} +1 -1
- package/src/assets/web-panel/assets/{index-__qol0SM.js → index-DaK59dFw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dfm65OiJ.js → index-DaYfMM7r.js} +1 -1
- package/src/assets/web-panel/assets/{index-B75QpKcS.js → index-De0xvTEv.js} +1 -1
- package/src/assets/web-panel/assets/{index-ByjAzHrC.js → index-DeoJYTHX.js} +4 -4
- package/src/assets/web-panel/assets/{index-OY_eegoA.js → index-Dj1N1idV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcbuseEC.js → index-DvWXdCxl.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQIzU6GO.js → index-DxHmNJ8S.js} +1 -1
- package/src/assets/web-panel/assets/{index-xOVop383.js → index-DxdLksCx.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmVrKala.js → index-KFdfLxVt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBSGKZ-l.js → index-S9g45I5s.js} +3 -3
- package/src/assets/web-panel/assets/{index-BKsNidiT.js → index-UId2JAY6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRPGyig2.js → index-UjxsY00y.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrJfwd9_.js → index-b7sSMzkC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqJoAOhC.js → index-l2VM-W36.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVTgMzbI.js → index-zP_o4yKV.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DjUNw-1y.js → initDefaultProps-BTGwT0NA.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Cd6xXLlf.js → motion-BheHkG8e.js} +1 -1
- package/src/assets/web-panel/assets/{move-B_W-Y6JL.js → move-BkMMNkAw.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BCZBLb_-.js → omit-B9zmSFf6.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CjDhGYLm.js → pickAttrs-CFNBcA5Z.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CSC2rU9-.js → placementArrow-B4mvrUYw.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DnyswFkD.js → responsiveObserve-BJqsSjxJ.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DujNhbuH.js → slide-Cj78MXSi.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-U2lNCQiL.js → statusUtils-Bk6ChXAV.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Ch2rQftE.js → styleChecker-B_YDSNYf.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-5XxNwjTL.js → useFlexGapSupport-q90prOQL.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-pv2YfF-X.js → useFs-BtpFJ9kI.js} +1 -1
- package/src/assets/web-panel/assets/usePersonalDataHub-C3qhf1sQ.js +1 -0
- package/src/assets/web-panel/assets/{vnode-CU0J2mVX.js → vnode-Bz2j0JiG.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-ZII_iqXl.js → zoom-CzaEtel2.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/__tests__/hub-ask.test.js +136 -0
- package/src/commands/ask.js +110 -43
- package/src/commands/hub.js +260 -26
- package/src/constants.js +7 -0
- package/src/gateways/ws/personal-data-hub-protocol.js +151 -9
- package/src/lib/host-adb-bridge.js +430 -0
- package/src/lib/personal-data-hub-wiring.js +85 -0
- package/src/assets/web-panel/assets/AppLayout-hku5aFqc.js +0 -3
- package/src/assets/web-panel/assets/PersonalDataHub-CmTx090l.js +0 -1
- package/src/assets/web-panel/assets/Privacy-CStpViYX.js +0 -1
- package/src/assets/web-panel/assets/Reputation-DSTNi8ge.js +0 -1
- package/src/assets/web-panel/assets/Trust-DX_BvTA9.js +0 -1
- package/src/assets/web-panel/assets/icons-BOPtEWK4.js +0 -57
- package/src/assets/web-panel/assets/index-B-xjGh0B.js +0 -1
- package/src/assets/web-panel/assets/index-B9RskCJ5.js +0 -1
|
@@ -22,12 +22,103 @@ import {
|
|
|
22
22
|
close as closeHub,
|
|
23
23
|
} from "../../lib/personal-data-hub-wiring.js";
|
|
24
24
|
import { getAIChatWizard } from "../../lib/personal-data-hub-aichat-wizard.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
existsSync,
|
|
27
|
+
unlinkSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
writeFileSync,
|
|
31
|
+
} from "node:fs";
|
|
26
32
|
import { join } from "node:path";
|
|
27
33
|
import pdhPkg from "@chainlesschain/personal-data-hub";
|
|
28
34
|
|
|
29
35
|
const { ingestSystemDataAndroidSnapshot } = pdhPkg;
|
|
30
36
|
|
|
37
|
+
/**
|
|
38
|
+
* If the caller didn't pass `inputPath`, try to pull a snapshot for
|
|
39
|
+
* this adapter off the attached Android phone via `adb shell run-as
|
|
40
|
+
* <pkg.debug> cat files/.chainlesschain/staging/<name>.json`. On
|
|
41
|
+
* success, write the JSON to `hub.hubDir/staging/` and return an
|
|
42
|
+
* `options` object with `inputPath` set so the adapter's _syncViaSnapshot
|
|
43
|
+
* picks it up.
|
|
44
|
+
*
|
|
45
|
+
* Why this is needed: every social adapter (bilibili, weibo, douyin,
|
|
46
|
+
* xiaohongshu, toutiao, kuaishou, qq, wechat, baidu-map, tencent-map,
|
|
47
|
+
* jd, meituan, pinduoduo) is snapshot-only and the Android in-app
|
|
48
|
+
* collectors write their JSON into the app's filesDir/.chainlesschain/
|
|
49
|
+
* staging/. Without an auto-pull, the desktop 同步 button can never
|
|
50
|
+
* reach a working state for these adapters — it just throws "needs
|
|
51
|
+
* opts.inputPath".
|
|
52
|
+
*
|
|
53
|
+
* Best-effort: if anything fails (no device, package not debuggable,
|
|
54
|
+
* snapshot file missing for that adapter, bridge module unavailable),
|
|
55
|
+
* return the original options unchanged so the adapter's normal error
|
|
56
|
+
* path fires and the UI banner shows a meaningful message.
|
|
57
|
+
*/
|
|
58
|
+
async function _tryAdbAutoPullInputPath(hub, name, options) {
|
|
59
|
+
if (
|
|
60
|
+
options &&
|
|
61
|
+
typeof options.inputPath === "string" &&
|
|
62
|
+
options.inputPath.length > 0
|
|
63
|
+
) {
|
|
64
|
+
return options; // caller already supplied a path
|
|
65
|
+
}
|
|
66
|
+
// Skip auto-pull for adapters that have a live bridge mode — pulling
|
|
67
|
+
// a stale snapshot file would short-circuit the bridge path which
|
|
68
|
+
// yields fresher / richer data (e.g. system-data-android's bridge
|
|
69
|
+
// mode pulls contacts + apps + sms + call_log live via ADB, while
|
|
70
|
+
// an Android-collected snapshot only contains contacts + apps).
|
|
71
|
+
// Keep this list in sync with adapters whose _syncViaBridge is
|
|
72
|
+
// strictly richer than their snapshot output.
|
|
73
|
+
// browser-history-* and vscode read desktop-local files (browser History
|
|
74
|
+
// SQLite + Bookmarks JSON, or VS Code workspaceStorage + state.vscdb);
|
|
75
|
+
// ADB snapshot is meaningless for them.
|
|
76
|
+
const BRIDGE_PREFERRED = new Set([
|
|
77
|
+
"system-data-android",
|
|
78
|
+
"browser-history-chrome",
|
|
79
|
+
"browser-history-edge",
|
|
80
|
+
"vscode",
|
|
81
|
+
"win-recent",
|
|
82
|
+
"git-activity",
|
|
83
|
+
"shell-history",
|
|
84
|
+
"local-files",
|
|
85
|
+
]);
|
|
86
|
+
if (BRIDGE_PREFERRED.has(name)) {
|
|
87
|
+
return options || {};
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const { createHostAdbBridge } =
|
|
91
|
+
await import("../../lib/host-adb-bridge.js");
|
|
92
|
+
const bridge = createHostAdbBridge();
|
|
93
|
+
const content = await bridge.invoke("snapshot.read", {
|
|
94
|
+
fileName: `${name}.json`,
|
|
95
|
+
});
|
|
96
|
+
// Sanity: must be parseable JSON. If not, the adapter would fail
|
|
97
|
+
// with a confusing parse error — surface a clearer one here.
|
|
98
|
+
try {
|
|
99
|
+
JSON.parse(content);
|
|
100
|
+
} catch (_e) {
|
|
101
|
+
return options; // leave to adapter — it'll report parse error
|
|
102
|
+
}
|
|
103
|
+
const stagingDir = join(hub.hubDir, "staging");
|
|
104
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
105
|
+
const stagingPath = join(
|
|
106
|
+
stagingDir,
|
|
107
|
+
`${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`,
|
|
108
|
+
);
|
|
109
|
+
writeFileSync(stagingPath, content, "utf-8");
|
|
110
|
+
return {
|
|
111
|
+
...(options || {}),
|
|
112
|
+
inputPath: stagingPath,
|
|
113
|
+
_autoPulledViaAdb: true,
|
|
114
|
+
};
|
|
115
|
+
} catch (_e) {
|
|
116
|
+
// Bridge unavailable / device not attached / file missing — let the
|
|
117
|
+
// adapter throw its normal error so the banner explains.
|
|
118
|
+
return options || {};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
31
122
|
async function withHub(fn) {
|
|
32
123
|
try {
|
|
33
124
|
const hub = await getHub();
|
|
@@ -83,10 +174,26 @@ export const PERSONAL_DATA_HUB_HANDLERS = {
|
|
|
83
174
|
withHub((hub) => hub.registry.list()),
|
|
84
175
|
|
|
85
176
|
"personal-data-hub.sync-adapter": async (msg) =>
|
|
86
|
-
withHub(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
177
|
+
withHub(async (hub) => {
|
|
178
|
+
const options = await _tryAdbAutoPullInputPath(
|
|
179
|
+
hub,
|
|
180
|
+
msg.name,
|
|
181
|
+
msg.options,
|
|
182
|
+
);
|
|
183
|
+
try {
|
|
184
|
+
return await hub.registry.syncAdapter(msg.name, options);
|
|
185
|
+
} finally {
|
|
186
|
+
// Best-effort cleanup of staging file we just wrote (don't shadow
|
|
187
|
+
// a real adapter error with cleanup noise).
|
|
188
|
+
if (options && options._autoPulledViaAdb && options.inputPath) {
|
|
189
|
+
try {
|
|
190
|
+
if (existsSync(options.inputPath)) unlinkSync(options.inputPath);
|
|
191
|
+
} catch (_e) {
|
|
192
|
+
/* ignore */
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}),
|
|
90
197
|
|
|
91
198
|
"personal-data-hub.sync-all": async (msg) =>
|
|
92
199
|
withHub(async (hub) => await hub.registry.syncAll(msg.options || {})),
|
|
@@ -116,6 +223,36 @@ export const PERSONAL_DATA_HUB_HANDLERS = {
|
|
|
116
223
|
}),
|
|
117
224
|
),
|
|
118
225
|
|
|
226
|
+
// Phase 16 Vault Browser — full-text + faceted search over events.
|
|
227
|
+
// See packages/personal-data-hub/lib/vault.js#searchEvents for the
|
|
228
|
+
// query/result shape. The desktop browser view + Android "我的数据" tab
|
|
229
|
+
// both consume this topic.
|
|
230
|
+
"personal-data-hub.search-events": async (msg) =>
|
|
231
|
+
withHub((hub) =>
|
|
232
|
+
hub.vault.searchEvents({
|
|
233
|
+
q: msg.q,
|
|
234
|
+
adapter: msg.adapter,
|
|
235
|
+
category: msg.category,
|
|
236
|
+
subtype: msg.subtype,
|
|
237
|
+
since: msg.since,
|
|
238
|
+
until: msg.until,
|
|
239
|
+
cursor: msg.cursor,
|
|
240
|
+
limit: msg.limit,
|
|
241
|
+
}),
|
|
242
|
+
),
|
|
243
|
+
|
|
244
|
+
// Phase 16 Vault Browser — counts grouped by category/adapter/subtype,
|
|
245
|
+
// honoring the same q + since/until filters as search-events. Powers
|
|
246
|
+
// the sidebar badges + adapter chip counts in the browser UI.
|
|
247
|
+
"personal-data-hub.facet-counts": async (msg) =>
|
|
248
|
+
withHub((hub) =>
|
|
249
|
+
hub.vault.facetCounts({
|
|
250
|
+
q: msg.q,
|
|
251
|
+
since: msg.since,
|
|
252
|
+
until: msg.until,
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
|
|
119
256
|
"personal-data-hub.recent-audit": async (msg) =>
|
|
120
257
|
withHub((hub) =>
|
|
121
258
|
hub.vault.queryAudit({
|
|
@@ -379,14 +516,19 @@ export const PERSONAL_DATA_HUB_STREAMING_HANDLERS = {
|
|
|
379
516
|
} catch (_e) {}
|
|
380
517
|
}
|
|
381
518
|
};
|
|
519
|
+
const options = await _tryAdbAutoPullInputPath(hub, msg.name, msg.options);
|
|
382
520
|
try {
|
|
383
|
-
const report = await hub.registry.syncAdapter(
|
|
384
|
-
msg.name,
|
|
385
|
-
msg.options || {},
|
|
386
|
-
);
|
|
521
|
+
const report = await hub.registry.syncAdapter(msg.name, options);
|
|
387
522
|
return { result: report };
|
|
388
523
|
} finally {
|
|
389
524
|
hub.registry.onSyncEvent = original;
|
|
525
|
+
if (options && options._autoPulledViaAdb && options.inputPath) {
|
|
526
|
+
try {
|
|
527
|
+
if (existsSync(options.inputPath)) unlinkSync(options.inputPath);
|
|
528
|
+
} catch (_e) {
|
|
529
|
+
/* ignore */
|
|
530
|
+
}
|
|
531
|
+
}
|
|
390
532
|
}
|
|
391
533
|
},
|
|
392
534
|
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host-adb-bridge — host-side `bridgeProvider` for the
|
|
3
|
+
* system-data-android PDH adapter (ESM mirror of the CJS module at
|
|
4
|
+
* desktop-app-vue/src/main/personal-data-hub/desktop-adb-bridge.js).
|
|
5
|
+
*
|
|
6
|
+
* The packaged `cc-android-bridge.js` only works INSIDE the Android cc
|
|
7
|
+
* subprocess (it dials a localhost HTTP server the Android app exposes).
|
|
8
|
+
* That path requires the user to launch the in-APK cc, which most
|
|
9
|
+
* users never do. This shim implements the same `invoke(method,
|
|
10
|
+
* params) → Promise<result>` surface but runs commands via the system
|
|
11
|
+
* `adb` (developer-mode USB debugging) instead.
|
|
12
|
+
*
|
|
13
|
+
* Lives in packages/cli because both `cc serve` / `cc ui` AND the
|
|
14
|
+
* desktop web-shell embed of the CLI WS server use this CLI's wiring
|
|
15
|
+
* (personal-data-hub-wiring.js) — putting the bridge here means both
|
|
16
|
+
* paths get the auto-engage behavior. The desktop-app-vue CJS copy is
|
|
17
|
+
* still used for the V5/V6 IPC code path (separate hub singleton).
|
|
18
|
+
*
|
|
19
|
+
* Methods implemented (only what system-data-android consumes):
|
|
20
|
+
* - `contacts.query({since?})` →
|
|
21
|
+
* [{ lookupKey, displayName }, ...]
|
|
22
|
+
* - `app.list({includeSystem?})` →
|
|
23
|
+
* [{ packageName }, ...]
|
|
24
|
+
*
|
|
25
|
+
* Caveats (cross-checked against the sjqz reference at C:\code\sjqz):
|
|
26
|
+
* - No root required for the two methods above.
|
|
27
|
+
* - User must approve "USB 调试授权" on the phone the first time.
|
|
28
|
+
* - 0 or >1 devices attached → clear typed error (UI surfaces it via
|
|
29
|
+
* the new SyncReport.error rendering).
|
|
30
|
+
* - Windows CRLF trap: JS `$` does NOT match before `\r`, only
|
|
31
|
+
* `\n` / EOS — every parse routine strips `\r+$` first.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { execFile } from "node:child_process";
|
|
35
|
+
import { promisify } from "node:util";
|
|
36
|
+
|
|
37
|
+
const execFileP = promisify(execFile);
|
|
38
|
+
|
|
39
|
+
export class HostAdbBridgeUnavailableError extends Error {
|
|
40
|
+
constructor(reason) {
|
|
41
|
+
super(`HOST_ADB_BRIDGE_NOT_AVAILABLE: ${reason}`);
|
|
42
|
+
this.code = "HOST_ADB_BRIDGE_NOT_AVAILABLE";
|
|
43
|
+
this.reason = reason;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function adb(args, opts = {}) {
|
|
48
|
+
const adbPath = opts.adbPath || process.env.ADB_PATH || "adb";
|
|
49
|
+
const fullArgs = opts.serial ? ["-s", opts.serial, ...args] : args;
|
|
50
|
+
try {
|
|
51
|
+
const { stdout, stderr } = await execFileP(adbPath, fullArgs, {
|
|
52
|
+
timeout: opts.timeoutMs || 30_000,
|
|
53
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
});
|
|
56
|
+
if (stderr && /error:|failed:|protocol fault/i.test(stderr)) {
|
|
57
|
+
throw new HostAdbBridgeUnavailableError(stderr.trim().split("\n")[0]);
|
|
58
|
+
}
|
|
59
|
+
return stdout;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e.code === "ENOENT") {
|
|
62
|
+
throw new HostAdbBridgeUnavailableError(
|
|
63
|
+
"adb binary not found on PATH (install Android Platform Tools, or set ADB_PATH)",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (e instanceof HostAdbBridgeUnavailableError) {
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
throw new HostAdbBridgeUnavailableError(e.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function listDevices(opts = {}) {
|
|
74
|
+
const stdout = await adb(["devices"], opts);
|
|
75
|
+
const lines = stdout.split("\n").slice(1);
|
|
76
|
+
const serials = [];
|
|
77
|
+
for (const rawLine of lines) {
|
|
78
|
+
const line = rawLine.replace(/\r+$/, "").trim();
|
|
79
|
+
if (!line) continue;
|
|
80
|
+
const [serial, state] = line.split(/\s+/);
|
|
81
|
+
if (state === "device") serials.push(serial);
|
|
82
|
+
}
|
|
83
|
+
return serials;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function pickDevice(opts = {}) {
|
|
87
|
+
if (opts.serial) return opts.serial;
|
|
88
|
+
if (process.env.ADB_SERIAL) return process.env.ADB_SERIAL;
|
|
89
|
+
const serials = await listDevices(opts);
|
|
90
|
+
if (serials.length === 0) {
|
|
91
|
+
throw new HostAdbBridgeUnavailableError(
|
|
92
|
+
"no Android device attached (enable USB debugging + plug in cable + approve 'USB 调试授权' on phone)",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (serials.length > 1) {
|
|
96
|
+
throw new HostAdbBridgeUnavailableError(
|
|
97
|
+
`multiple devices attached (${serials.join(", ")}); set ADB_SERIAL=<serial> to disambiguate`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return serials[0];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseContentQueryRows(stdout) {
|
|
104
|
+
const rows = [];
|
|
105
|
+
for (const rawLine of stdout.split("\n")) {
|
|
106
|
+
const line = rawLine.replace(/\r+$/, "");
|
|
107
|
+
const m = line.match(/^Row:\s+\d+\s+(.*)$/);
|
|
108
|
+
if (!m) continue;
|
|
109
|
+
const fields = {};
|
|
110
|
+
const fieldRe =
|
|
111
|
+
/([A-Za-z_][A-Za-z0-9_]*)=([^,]*?)(?=,\s+[A-Za-z_][A-Za-z0-9_]*=|$)/g;
|
|
112
|
+
let fm;
|
|
113
|
+
while ((fm = fieldRe.exec(m[1])) !== null) {
|
|
114
|
+
const key = fm[1];
|
|
115
|
+
const val = fm[2];
|
|
116
|
+
fields[key] = val === "NULL" ? null : val;
|
|
117
|
+
}
|
|
118
|
+
rows.push(fields);
|
|
119
|
+
}
|
|
120
|
+
return rows;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function queryContacts(_params, opts) {
|
|
124
|
+
const serial = await pickDevice(opts);
|
|
125
|
+
const stdout = await adb(
|
|
126
|
+
[
|
|
127
|
+
"shell",
|
|
128
|
+
"content",
|
|
129
|
+
"query",
|
|
130
|
+
"--uri",
|
|
131
|
+
"content://com.android.contacts/contacts",
|
|
132
|
+
"--projection",
|
|
133
|
+
"lookup:display_name",
|
|
134
|
+
],
|
|
135
|
+
{ ...opts, serial },
|
|
136
|
+
);
|
|
137
|
+
const rows = parseContentQueryRows(stdout);
|
|
138
|
+
return rows
|
|
139
|
+
.map((r) => ({
|
|
140
|
+
lookupKey: r.lookup || null,
|
|
141
|
+
displayName: r.display_name || null,
|
|
142
|
+
}))
|
|
143
|
+
.filter((c) => c.displayName);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function listApps(params, opts) {
|
|
147
|
+
const serial = await pickDevice(opts);
|
|
148
|
+
const includeSystem = params && params.includeSystem === true;
|
|
149
|
+
const flag = includeSystem ? "" : "-3";
|
|
150
|
+
const argv = ["shell", "pm", "list", "packages"];
|
|
151
|
+
if (flag) argv.push(flag);
|
|
152
|
+
const stdout = await adb(argv, { ...opts, serial });
|
|
153
|
+
const apps = [];
|
|
154
|
+
for (const rawLine of stdout.split("\n")) {
|
|
155
|
+
const line = rawLine.replace(/\r+$/, "");
|
|
156
|
+
const m = line.match(/^package:(.+)$/);
|
|
157
|
+
if (m) apps.push({ packageName: m[1].trim() });
|
|
158
|
+
}
|
|
159
|
+
return apps;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Query SMS via the system content provider. No root, no permissions
|
|
164
|
+
* declared by host — ADB shell user already has READ_SMS-equivalent
|
|
165
|
+
* access. Returns one row per message. The body can contain commas;
|
|
166
|
+
* parseContentQueryRows treats `, <ident>=` as the field boundary so
|
|
167
|
+
* naturally-written text doesn't break the parse (only adversarial
|
|
168
|
+
* SMS containing `, X=` would).
|
|
169
|
+
*
|
|
170
|
+
* @returns {Promise<Array<{id, address, body, date, dateSent, type, threadId, read, subject}>>}
|
|
171
|
+
*/
|
|
172
|
+
async function querySms(params, opts) {
|
|
173
|
+
const serial = await pickDevice(opts);
|
|
174
|
+
const stdout = await adb(
|
|
175
|
+
["shell", "content", "query", "--uri", "content://sms"],
|
|
176
|
+
{ ...opts, serial, timeoutMs: opts.timeoutMs || 120_000 },
|
|
177
|
+
);
|
|
178
|
+
const rows = parseContentQueryRows(stdout);
|
|
179
|
+
return rows
|
|
180
|
+
.map((r) => ({
|
|
181
|
+
id: r._id ? String(r._id) : null,
|
|
182
|
+
address: r.address || null,
|
|
183
|
+
body: r.body || null,
|
|
184
|
+
date: r.date ? parseInt(r.date, 10) : null,
|
|
185
|
+
dateSent: r.date_sent ? parseInt(r.date_sent, 10) : null,
|
|
186
|
+
// SMS type: 1=inbox, 2=sent, 3=draft, 4=outbox, 5=failed, 6=queued
|
|
187
|
+
type: r.type ? parseInt(r.type, 10) : null,
|
|
188
|
+
threadId: r.thread_id ? parseInt(r.thread_id, 10) : null,
|
|
189
|
+
read: r.read === "1" ? true : r.read === "0" ? false : null,
|
|
190
|
+
subject: r.subject || null,
|
|
191
|
+
}))
|
|
192
|
+
.filter((m) => m.id); // drop rows with no _id (malformed)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Query call log via the system content provider. Same access model
|
|
197
|
+
* as SMS. Returns one row per call.
|
|
198
|
+
*
|
|
199
|
+
* @returns {Promise<Array<{id, number, name, duration, date, type, geocoded}>>}
|
|
200
|
+
*/
|
|
201
|
+
async function queryCallLog(params, opts) {
|
|
202
|
+
const serial = await pickDevice(opts);
|
|
203
|
+
const stdout = await adb(
|
|
204
|
+
["shell", "content", "query", "--uri", "content://call_log/calls"],
|
|
205
|
+
{ ...opts, serial, timeoutMs: opts.timeoutMs || 120_000 },
|
|
206
|
+
);
|
|
207
|
+
const rows = parseContentQueryRows(stdout);
|
|
208
|
+
return rows
|
|
209
|
+
.map((r) => ({
|
|
210
|
+
id: r._id ? String(r._id) : null,
|
|
211
|
+
number: r.number || null,
|
|
212
|
+
name: r.name || null,
|
|
213
|
+
// Call duration in seconds
|
|
214
|
+
duration: r.duration ? parseInt(r.duration, 10) : null,
|
|
215
|
+
date: r.date ? parseInt(r.date, 10) : null,
|
|
216
|
+
// Call type: 1=incoming, 2=outgoing, 3=missed, 4=voicemail, 5=rejected, 6=blocked
|
|
217
|
+
type: r.type ? parseInt(r.type, 10) : null,
|
|
218
|
+
geocoded: r.geocoded_location || null,
|
|
219
|
+
}))
|
|
220
|
+
.filter((c) => c.id);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Lists media files in well-known /sdcard subdirectories. Returns metadata
|
|
225
|
+
* only — file CONTENT is never read off the device through this method.
|
|
226
|
+
* Pure ADB shell, no root, no permissions declared (adb shell user already
|
|
227
|
+
* has READ_EXTERNAL_STORAGE-equivalent access).
|
|
228
|
+
*
|
|
229
|
+
* Category → directory mapping:
|
|
230
|
+
* photos /sdcard/DCIM/Camera
|
|
231
|
+
* pictures /sdcard/Pictures (screenshots, downloaded images, etc.)
|
|
232
|
+
* videos /sdcard/Movies
|
|
233
|
+
* downloads /sdcard/Download
|
|
234
|
+
* documents /sdcard/Documents
|
|
235
|
+
*
|
|
236
|
+
* Uses `find -printf "%s\t%T@\t%p\n"` which is supported by Android toybox.
|
|
237
|
+
* One subprocess per category — concurrent isn't worth it; even ~5000-file
|
|
238
|
+
* Pictures listings finish in <2s.
|
|
239
|
+
*
|
|
240
|
+
* @param {{category: string, since?: number}} params
|
|
241
|
+
* - category: one of the keys above (required)
|
|
242
|
+
* - since: epoch-ms; files with mtime < since are skipped
|
|
243
|
+
* @returns {Promise<Array<{path, size, mtimeMs, ext, category}>>}
|
|
244
|
+
*/
|
|
245
|
+
const MEDIA_DIRS = {
|
|
246
|
+
photos: "/sdcard/DCIM/Camera",
|
|
247
|
+
pictures: "/sdcard/Pictures",
|
|
248
|
+
videos: "/sdcard/Movies",
|
|
249
|
+
downloads: "/sdcard/Download",
|
|
250
|
+
documents: "/sdcard/Documents",
|
|
251
|
+
};
|
|
252
|
+
async function listMedia(params, opts) {
|
|
253
|
+
const cat = params && params.category;
|
|
254
|
+
const dir = MEDIA_DIRS[cat];
|
|
255
|
+
if (!dir) {
|
|
256
|
+
throw new HostAdbBridgeUnavailableError(
|
|
257
|
+
`media.list: unknown category "${cat}". Valid: ${Object.keys(MEDIA_DIRS).join(", ")}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const serial = await pickDevice(opts);
|
|
261
|
+
const stdout = await adb(
|
|
262
|
+
[
|
|
263
|
+
"shell",
|
|
264
|
+
// Suppress "Permission denied" stderr noise on a few system-protected
|
|
265
|
+
// subdirs (e.g. .trashed). 2>/dev/null hides those rows from output.
|
|
266
|
+
`find ${dir} -type f -printf '%s\\t%T@\\t%p\\n' 2>/dev/null`,
|
|
267
|
+
],
|
|
268
|
+
{ ...opts, serial, timeoutMs: opts.timeoutMs || 180_000 },
|
|
269
|
+
);
|
|
270
|
+
const sinceMs = Number.isInteger(params?.since) ? params.since : 0;
|
|
271
|
+
const out = [];
|
|
272
|
+
for (const rawLine of stdout.split("\n")) {
|
|
273
|
+
const line = rawLine.replace(/\r+$/, "");
|
|
274
|
+
if (!line) continue;
|
|
275
|
+
// tab-separated: <size>\t<mtime_epoch_fractional>\t<path>
|
|
276
|
+
const tab1 = line.indexOf("\t");
|
|
277
|
+
const tab2 = line.indexOf("\t", tab1 + 1);
|
|
278
|
+
if (tab1 < 0 || tab2 < 0) continue;
|
|
279
|
+
const size = parseInt(line.substring(0, tab1), 10);
|
|
280
|
+
const mtimeSec = parseFloat(line.substring(tab1 + 1, tab2));
|
|
281
|
+
const path = line.substring(tab2 + 1);
|
|
282
|
+
if (!Number.isFinite(size) || !Number.isFinite(mtimeSec) || !path) continue;
|
|
283
|
+
// Filter out hidden / system files — any path segment starts with "."
|
|
284
|
+
// catches .thumbnails/, .trashed-*/, .nomedia, etc.
|
|
285
|
+
if (path.split("/").some((seg) => seg.startsWith("."))) continue;
|
|
286
|
+
const mtimeMs = Math.floor(mtimeSec * 1000);
|
|
287
|
+
if (sinceMs > 0 && mtimeMs < sinceMs) continue;
|
|
288
|
+
const lastDot = path.lastIndexOf(".");
|
|
289
|
+
const ext = lastDot >= 0 ? path.substring(lastDot + 1).toLowerCase() : "";
|
|
290
|
+
out.push({ path, size, mtimeMs, ext, category: cat });
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* List snapshot JSON files in the Android app's staging directory.
|
|
297
|
+
* Uses `adb shell run-as` so only works on debuggable builds (which is
|
|
298
|
+
* always true for `<pkg>.debug` variants). Production builds will
|
|
299
|
+
* surface `package not debuggable` — UI can fall back to a manual
|
|
300
|
+
* upload path.
|
|
301
|
+
*
|
|
302
|
+
* @param {{packageName?: string}} params
|
|
303
|
+
* - packageName: defaults to `com.chainlesschain.android.debug`. The
|
|
304
|
+
* release variant `com.chainlesschain.android` is NOT readable via
|
|
305
|
+
* run-as.
|
|
306
|
+
* @returns {Promise<Array<{name, sizeBytes}>>}
|
|
307
|
+
*/
|
|
308
|
+
async function listSnapshots(params, opts) {
|
|
309
|
+
const serial = await pickDevice(opts);
|
|
310
|
+
const pkg =
|
|
311
|
+
(params && params.packageName) || "com.chainlesschain.android.debug";
|
|
312
|
+
const stdout = await adb(
|
|
313
|
+
["shell", "run-as", pkg, "ls", "-la", "files/.chainlesschain/staging/"],
|
|
314
|
+
{ ...opts, serial },
|
|
315
|
+
);
|
|
316
|
+
if (/run-as: package not debuggable/.test(stdout)) {
|
|
317
|
+
throw new HostAdbBridgeUnavailableError(
|
|
318
|
+
`${pkg} is not debuggable — only debug-variant packages expose staging via run-as`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const out = [];
|
|
322
|
+
for (const rawLine of stdout.split("\n")) {
|
|
323
|
+
const line = rawLine.replace(/\r+$/, "").trim();
|
|
324
|
+
if (!line || line.startsWith("total ") || line.startsWith("d")) continue;
|
|
325
|
+
// ls -la row: `-rw------- 1 u0_a395 u0_a395 204110 2026-05-23 13:47 system-data-android.json`
|
|
326
|
+
const m = line.match(
|
|
327
|
+
/^[-l]\S*\s+\d+\s+\S+\s+\S+\s+(\d+)\s+\S+\s+\S+\s+(.+\.json)$/,
|
|
328
|
+
);
|
|
329
|
+
if (m) out.push({ name: m[2], sizeBytes: parseInt(m[1], 10) });
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Read a snapshot file's content from the Android app's staging directory.
|
|
336
|
+
* Returns the raw UTF-8 text (the JSON the adapter's _syncViaSnapshot
|
|
337
|
+
* expects). Same debuggable-only constraint as listSnapshots.
|
|
338
|
+
*
|
|
339
|
+
* @param {{packageName?: string, fileName: string}} params
|
|
340
|
+
* @returns {Promise<string>}
|
|
341
|
+
*/
|
|
342
|
+
async function readSnapshot(params, opts) {
|
|
343
|
+
if (
|
|
344
|
+
!params ||
|
|
345
|
+
typeof params.fileName !== "string" ||
|
|
346
|
+
!params.fileName.endsWith(".json")
|
|
347
|
+
) {
|
|
348
|
+
throw new HostAdbBridgeUnavailableError(
|
|
349
|
+
"readSnapshot: params.fileName must be a .json filename inside the staging dir",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
// Defense-in-depth: reject path traversal — fileName must be a bare
|
|
353
|
+
// basename, no slashes / dots-leading.
|
|
354
|
+
if (params.fileName.includes("/") || params.fileName.startsWith(".")) {
|
|
355
|
+
throw new HostAdbBridgeUnavailableError(
|
|
356
|
+
`readSnapshot: refusing suspicious fileName "${params.fileName}"`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const serial = await pickDevice(opts);
|
|
360
|
+
const pkg = params.packageName || "com.chainlesschain.android.debug";
|
|
361
|
+
const stdout = await adb(
|
|
362
|
+
[
|
|
363
|
+
"shell",
|
|
364
|
+
"run-as",
|
|
365
|
+
pkg,
|
|
366
|
+
"cat",
|
|
367
|
+
`files/.chainlesschain/staging/${params.fileName}`,
|
|
368
|
+
],
|
|
369
|
+
{ ...opts, serial, timeoutMs: opts.timeoutMs || 60_000 },
|
|
370
|
+
);
|
|
371
|
+
if (/run-as: package not debuggable/.test(stdout)) {
|
|
372
|
+
throw new HostAdbBridgeUnavailableError(
|
|
373
|
+
`${pkg} is not debuggable — only debug variant exposes staging via run-as`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (/No such file or directory/.test(stdout)) {
|
|
377
|
+
throw new HostAdbBridgeUnavailableError(
|
|
378
|
+
`snapshot ${params.fileName} not found in ${pkg}'s staging dir (collector probably never ran for this adapter)`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return stdout;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function createHostAdbBridge(opts = {}) {
|
|
385
|
+
return {
|
|
386
|
+
/**
|
|
387
|
+
* SystemDataAndroidAdapter._bridgeAvailable() reads this; must be
|
|
388
|
+
* sync. We report available:true optimistically — invoke() will
|
|
389
|
+
* throw a typed error if ADB / device isn't usable, which the
|
|
390
|
+
* registry surfaces via SyncReport.error (rendered by
|
|
391
|
+
* PersonalDataHub.vue syncSummary).
|
|
392
|
+
*/
|
|
393
|
+
caps() {
|
|
394
|
+
return { available: true };
|
|
395
|
+
},
|
|
396
|
+
async invoke(method, params = {}) {
|
|
397
|
+
switch (method) {
|
|
398
|
+
case "contacts.query":
|
|
399
|
+
return await queryContacts(params, opts);
|
|
400
|
+
case "app.list":
|
|
401
|
+
return await listApps(params, opts);
|
|
402
|
+
case "sms.query":
|
|
403
|
+
return await querySms(params, opts);
|
|
404
|
+
case "call.query":
|
|
405
|
+
return await queryCallLog(params, opts);
|
|
406
|
+
case "media.list":
|
|
407
|
+
return await listMedia(params, opts);
|
|
408
|
+
case "snapshot.list":
|
|
409
|
+
return await listSnapshots(params, opts);
|
|
410
|
+
case "snapshot.read":
|
|
411
|
+
return await readSnapshot(params, opts);
|
|
412
|
+
default:
|
|
413
|
+
throw new HostAdbBridgeUnavailableError(
|
|
414
|
+
`method "${method}" not implemented by host-adb-bridge`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Exposed for unit testing without spawning real adb.
|
|
422
|
+
export const _internals = {
|
|
423
|
+
parseContentQueryRows,
|
|
424
|
+
listDevices,
|
|
425
|
+
pickDevice,
|
|
426
|
+
queryContacts,
|
|
427
|
+
listApps,
|
|
428
|
+
listSnapshots,
|
|
429
|
+
readSnapshot,
|
|
430
|
+
};
|