@chainlesschain/personal-data-hub 0.2.0 → 0.2.1
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/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +211 -0
- package/__tests__/adapters/ai-chat-health-checker.test.js +262 -0
- package/__tests__/adapters/ai-chat-history.test.js +8 -7
- package/__tests__/adapters/ai-chat-vendors.test.js +149 -8
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +269 -0
- package/__tests__/adapters/system-data-android-ingest.test.js +144 -0
- package/__tests__/adapters/system-data-android.test.js +387 -0
- package/__tests__/adapters/wechat-bootstrap.test.js +240 -0
- package/__tests__/adapters/wechat-env-probe.test.js +162 -0
- package/__tests__/adapters/wechat-frida-agent.test.js +191 -0
- package/__tests__/adapters/wechat-frida-integration.test.js +149 -0
- package/__tests__/adapters/wechat-frida-key-provider.test.js +188 -0
- package/__tests__/adapters/wechat-md5-key-provider.test.js +101 -0
- package/__tests__/analysis-skills.test.js +147 -0
- package/__tests__/analysis.test.js +329 -1
- package/__tests__/e2e/ai-chat-cross-source-journey.test.js +213 -0
- package/__tests__/e2e/full-user-journey.test.js +188 -0
- package/__tests__/integration/ai-chat-history-registry.test.js +228 -0
- package/__tests__/integration/aichat-wizard-end-to-end.test.js +282 -0
- package/__tests__/integration/cross-adapter-pipelines.test.js +396 -0
- package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +390 -0
- package/__tests__/registry.test.js +4 -2
- package/lib/adapters/ai-chat-history/ai-chat-adapter.js +55 -16
- package/lib/adapters/ai-chat-history/cookie-capture-spec.js +331 -0
- package/lib/adapters/ai-chat-history/health-checker.js +210 -0
- package/lib/adapters/ai-chat-history/schema-map.js +42 -5
- package/lib/adapters/ai-chat-history/vendor-spec.js +1 -0
- package/lib/adapters/ai-chat-history/vendors/doubao.js +255 -0
- package/lib/adapters/ai-chat-history/wizard-controller.js +473 -0
- package/lib/adapters/alipay-bill/alipay-bill-adapter.js +4 -0
- package/lib/adapters/social-kuaishou/index.js +237 -0
- package/lib/adapters/social-toutiao/index.js +236 -0
- package/lib/adapters/system-data-android/adapter.js +348 -0
- package/lib/adapters/system-data-android/index.js +76 -0
- package/lib/adapters/wechat/bootstrap.js +146 -0
- package/lib/adapters/wechat/env-probe.js +218 -0
- package/lib/adapters/wechat/frida-agent/loader.js +67 -0
- package/lib/adapters/wechat/frida-agent/wechat-key-hook.js +126 -0
- package/lib/adapters/wechat/index.js +9 -0
- package/lib/adapters/wechat/key-providers/frida-key-provider.js +244 -0
- package/lib/adapters/wechat/key-providers/index.js +22 -0
- package/lib/adapters/wechat/key-providers/key-provider-base.js +44 -0
- package/lib/adapters/wechat/key-providers/md5-key-provider.js +81 -0
- package/lib/analysis-skills/spending.js +4 -1
- package/lib/analysis.js +191 -2
- package/lib/index.js +16 -0
- package/lib/prompt-builder.js +11 -1
- package/lib/query-parser.js +7 -1
- package/lib/vault.js +77 -0
- package/package.json +8 -1
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIChat WebView 鉴权向导 — Wizard Controller (Phase 10.3.2)
|
|
3
|
+
*
|
|
4
|
+
* Lives in the Electron main process. Drives the 4-step rendering-process
|
|
5
|
+
* Wizard for adding any of the 9 国产 AI vendors as a data source:
|
|
6
|
+
*
|
|
7
|
+
* openVendorLogin — provision a per-vendor session partition + (on
|
|
8
|
+
* desktop) return enough metadata for the renderer to
|
|
9
|
+
* host a `<webview>` / BrowserView. Web-shell mode is
|
|
10
|
+
* a no-op that returns `fallbackMode: "paste"`.
|
|
11
|
+
* probeCookies — read all cookies under the vendor's cookieDomains
|
|
12
|
+
* and classify against the spec.
|
|
13
|
+
* registerVendor — wrap AIChatHistoryAdapter's existing register path
|
|
14
|
+
* with cookie validation + accounts.json persistence
|
|
15
|
+
* (mirrors the shape used by Email / Alipay wiring).
|
|
16
|
+
* rotateLoginPartition — flush stored cookies + provision a fresh session.
|
|
17
|
+
* cleanupOrphanPartitions — startup-time scan that drops any
|
|
18
|
+
* persist:aichat-<vendor> partitions whose vendor is
|
|
19
|
+
* no longer in accounts.json.
|
|
20
|
+
*
|
|
21
|
+
* Reference: docs/design/Personal_Data_Hub_Phase_10_3_AIChat_WebView_Wizard.md §2 §4
|
|
22
|
+
*
|
|
23
|
+
* Electron access is fully behind `_deps` injection (sessionFactory /
|
|
24
|
+
* accountsStore / clock / logger / classifier). The Electron singletons are
|
|
25
|
+
* resolved lazily at construction time so tests stay Electron-free and
|
|
26
|
+
* Phase 10.3.3 (renderer wiring) can stub anything per test.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
"use strict";
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
getSpec,
|
|
33
|
+
listVendors,
|
|
34
|
+
classifyProbedCookies,
|
|
35
|
+
COOKIE_SPEC_VERSION,
|
|
36
|
+
} = require("./cookie-capture-spec");
|
|
37
|
+
|
|
38
|
+
const PARTITION_PREFIX = "persist:aichat-";
|
|
39
|
+
|
|
40
|
+
function _partitionNameFor(vendor) {
|
|
41
|
+
return PARTITION_PREFIX + vendor;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function _isAichatPartition(name) {
|
|
45
|
+
return typeof name === "string" && name.startsWith(PARTITION_PREFIX);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _vendorFromPartition(name) {
|
|
49
|
+
return _isAichatPartition(name) ? name.slice(PARTITION_PREFIX.length) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Default dep resolver — only loaded when running inside Electron. Tests pass
|
|
54
|
+
* an explicit `_deps` and never trigger this.
|
|
55
|
+
*/
|
|
56
|
+
function _resolveElectronDeps() {
|
|
57
|
+
// Lazy require — keeps this module unit-testable in plain Node.
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
59
|
+
const electron = require("electron");
|
|
60
|
+
const fs = require("node:fs");
|
|
61
|
+
return {
|
|
62
|
+
sessionFactory: (partition) => electron.session.fromPartition(partition, { cache: true }),
|
|
63
|
+
fs,
|
|
64
|
+
clock: () => Date.now(),
|
|
65
|
+
logger: {
|
|
66
|
+
info: (...a) => console.info("[aichat-wizard]", ...a),
|
|
67
|
+
warn: (...a) => console.warn("[aichat-wizard]", ...a),
|
|
68
|
+
error: (...a) => console.error("[aichat-wizard]", ...a),
|
|
69
|
+
},
|
|
70
|
+
classifier: classifyProbedCookies,
|
|
71
|
+
specLookup: getSpec,
|
|
72
|
+
knownVendors: listVendors(),
|
|
73
|
+
cookieSpecVersion: COOKIE_SPEC_VERSION,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Construct a controller bound to:
|
|
79
|
+
* - an accountsStore ({ get(vendor), put(vendor, entry), delete(vendor), list() })
|
|
80
|
+
* which the desktop wiring resolves to an `aichat-accounts.json` reader.
|
|
81
|
+
* - a vendorAdapter (the AIChatHistoryAdapter or any object with
|
|
82
|
+
* `registerVendor(vendor, cookies, opts)` returning a Promise).
|
|
83
|
+
*
|
|
84
|
+
* `_deps` is the seam for tests / future shells (web-shell injects a
|
|
85
|
+
* `fallbackMode: "paste"` sessionFactory that doesn't open BrowserView).
|
|
86
|
+
*/
|
|
87
|
+
function createAIChatWizardController({ accountsStore, vendorAdapter, _deps } = {}) {
|
|
88
|
+
if (!accountsStore || typeof accountsStore.get !== "function") {
|
|
89
|
+
throw new Error("aichat-wizard: accountsStore with get/put/delete/list required");
|
|
90
|
+
}
|
|
91
|
+
if (!vendorAdapter || typeof vendorAdapter.registerVendor !== "function") {
|
|
92
|
+
throw new Error("aichat-wizard: vendorAdapter.registerVendor required");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const deps = _deps || _resolveElectronDeps();
|
|
96
|
+
const {
|
|
97
|
+
sessionFactory, clock, logger, classifier, specLookup, knownVendors, cookieSpecVersion,
|
|
98
|
+
fallbackMode, // "browser-view" (default) or "paste" — web-shell sets this
|
|
99
|
+
} = deps;
|
|
100
|
+
|
|
101
|
+
function _requireSpec(vendor) {
|
|
102
|
+
const spec = specLookup(vendor);
|
|
103
|
+
if (!spec) {
|
|
104
|
+
const err = new Error("UNKNOWN_VENDOR");
|
|
105
|
+
err.code = "UNKNOWN_VENDOR";
|
|
106
|
+
err.vendor = vendor;
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
return spec;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Step 1 of the wizard.
|
|
114
|
+
*
|
|
115
|
+
* Returns the metadata the renderer needs to either host a BrowserView
|
|
116
|
+
* (desktop) or display the paste-fallback (web-shell). NEVER throws on
|
|
117
|
+
* unknown vendor — returns `{ ok:false, reason:"UNKNOWN_VENDOR" }` so the
|
|
118
|
+
* renderer can show a typed error.
|
|
119
|
+
*/
|
|
120
|
+
async function openVendorLogin({ vendor, opts = {} } = {}) {
|
|
121
|
+
const spec = specLookup(vendor);
|
|
122
|
+
if (!spec) {
|
|
123
|
+
return { ok: false, reason: "UNKNOWN_VENDOR", vendor };
|
|
124
|
+
}
|
|
125
|
+
const partition = _partitionNameFor(vendor);
|
|
126
|
+
|
|
127
|
+
// Web-shell path: no BrowserView, return paste helper instructions.
|
|
128
|
+
if (fallbackMode === "paste") {
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
vendor,
|
|
132
|
+
fallbackMode: "paste",
|
|
133
|
+
helpText:
|
|
134
|
+
`请在外部浏览器打开 ${spec.loginUrl} 完成登录,` +
|
|
135
|
+
`登录后从开发者工具 Application → Cookies 复制全部 cookie 串粘贴到下方文本框。`,
|
|
136
|
+
loginUrl: spec.loginUrl,
|
|
137
|
+
requiredCookies: spec.requiredCookies.slice(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Desktop path: prime the session (cookies are persisted automatically
|
|
142
|
+
// by Electron once the partition exists). The renderer is responsible
|
|
143
|
+
// for mounting a BrowserView pointing at `loginUrl` with `partition`.
|
|
144
|
+
let session;
|
|
145
|
+
try {
|
|
146
|
+
session = sessionFactory(partition);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
logger.error("openVendorLogin: sessionFactory failed", err);
|
|
149
|
+
return { ok: false, reason: "SESSION_INIT_FAILED", error: err.message };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Optional: reset stored cookies if the caller wants a clean login.
|
|
153
|
+
if (opts.reuseSession === false && typeof session.clearStorageData === "function") {
|
|
154
|
+
try {
|
|
155
|
+
await session.clearStorageData({ storages: ["cookies"] });
|
|
156
|
+
} catch (err) {
|
|
157
|
+
logger.warn("openVendorLogin: clearStorageData failed (ignored)", err);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
vendor,
|
|
164
|
+
fallbackMode: "browser-view",
|
|
165
|
+
partition,
|
|
166
|
+
loginUrl: spec.loginUrl,
|
|
167
|
+
cookieDomains: spec.cookieDomains.slice(),
|
|
168
|
+
postLoginPathHints: spec.postLoginPathHints.slice(),
|
|
169
|
+
notes: spec.notes,
|
|
170
|
+
openedAt: clock(),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Step 2 of the wizard.
|
|
176
|
+
*
|
|
177
|
+
* Reads cookies from the vendor's partition (desktop) or parses the pasted
|
|
178
|
+
* cookie string (web-shell), classifies them against the spec, and returns
|
|
179
|
+
* everything the renderer needs to show Step 3's validation summary.
|
|
180
|
+
*/
|
|
181
|
+
async function probeCookies({ vendor, cookieHeader } = {}) {
|
|
182
|
+
const spec = specLookup(vendor);
|
|
183
|
+
if (!spec) {
|
|
184
|
+
return { ok: false, reason: "UNKNOWN_VENDOR", vendor };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Web-shell paste path takes precedence — if a header was supplied, use it.
|
|
188
|
+
if (typeof cookieHeader === "string" && cookieHeader.length > 0) {
|
|
189
|
+
const classified = classifier(vendor, cookieHeader);
|
|
190
|
+
return {
|
|
191
|
+
ok: classified.ok,
|
|
192
|
+
vendor,
|
|
193
|
+
source: "paste",
|
|
194
|
+
cookies: _projectCookies(classified, cookieHeader, spec),
|
|
195
|
+
foundRequired: classified.foundRequired,
|
|
196
|
+
missingRequired: classified.missingRequired,
|
|
197
|
+
foundOptional: classified.foundOptional,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (fallbackMode === "paste") {
|
|
202
|
+
// Web-shell asked to probe without supplying a header — return guidance.
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
vendor,
|
|
206
|
+
reason: "PASTE_REQUIRED",
|
|
207
|
+
source: "paste",
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Desktop: ask each cookieDomain in turn for cookies.
|
|
212
|
+
const partition = _partitionNameFor(vendor);
|
|
213
|
+
let session;
|
|
214
|
+
try {
|
|
215
|
+
session = sessionFactory(partition);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
logger.error("probeCookies: sessionFactory failed", err);
|
|
218
|
+
return { ok: false, reason: "SESSION_INIT_FAILED", error: err.message };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const jar = {};
|
|
222
|
+
for (const domain of spec.cookieDomains) {
|
|
223
|
+
let cookies = [];
|
|
224
|
+
try {
|
|
225
|
+
// Electron `cookies.get({ domain })` returns Cookie[]. We pass
|
|
226
|
+
// domain stripped of leading dot when querying (Electron accepts both).
|
|
227
|
+
cookies = await session.cookies.get({ domain: domain.replace(/^\./, "") });
|
|
228
|
+
} catch (err) {
|
|
229
|
+
logger.warn(`probeCookies: cookies.get failed for domain=${domain}`, err);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
for (const c of cookies) {
|
|
233
|
+
if (c && typeof c.name === "string" && typeof c.value === "string" && c.value.length > 0) {
|
|
234
|
+
// Last write wins — domain order is intentional (root first via spec order).
|
|
235
|
+
jar[c.name] = c.value;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const classified = classifier(vendor, jar);
|
|
241
|
+
return {
|
|
242
|
+
ok: classified.ok,
|
|
243
|
+
vendor,
|
|
244
|
+
source: "browser-view",
|
|
245
|
+
cookies: jar,
|
|
246
|
+
foundRequired: classified.foundRequired,
|
|
247
|
+
missingRequired: classified.missingRequired,
|
|
248
|
+
foundOptional: classified.foundOptional,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Step 3 of the wizard.
|
|
254
|
+
*
|
|
255
|
+
* Re-classifies the supplied cookies one more time defensively (the
|
|
256
|
+
* renderer may have come from `probeCookies` long enough ago to drift),
|
|
257
|
+
* hands the jar off to AIChatHistoryAdapter.registerVendor, then persists
|
|
258
|
+
* a row in accounts.json on success. Returns `{ ok, validation, accountId }`.
|
|
259
|
+
*/
|
|
260
|
+
async function registerVendor({ vendor, cookies, opts = {} } = {}) {
|
|
261
|
+
const spec = specLookup(vendor);
|
|
262
|
+
if (!spec) {
|
|
263
|
+
return { ok: false, reason: "UNKNOWN_VENDOR", vendor };
|
|
264
|
+
}
|
|
265
|
+
const classified = classifier(vendor, cookies);
|
|
266
|
+
if (!classified.ok) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
vendor,
|
|
270
|
+
reason: "REQUIRED_COOKIES_MISSING",
|
|
271
|
+
missingRequired: classified.missingRequired,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let validation;
|
|
276
|
+
try {
|
|
277
|
+
validation = await vendorAdapter.registerVendor(vendor, _flattenJar(cookies), opts);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
logger.error("registerVendor: vendorAdapter threw", err);
|
|
280
|
+
return { ok: false, vendor, reason: "ADAPTER_THREW", error: err.message };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!validation || validation.ok !== true) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
vendor,
|
|
287
|
+
reason: (validation && validation.reason) || "VALIDATE_COOKIE_FAILED",
|
|
288
|
+
validation,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const now = clock();
|
|
293
|
+
const accountId = `${vendor}:${validation.userId || "anon"}`;
|
|
294
|
+
const entry = {
|
|
295
|
+
vendor,
|
|
296
|
+
registeredAt: (await _existingRegisteredAt(vendor)) || now,
|
|
297
|
+
cookies: _flattenJar(cookies),
|
|
298
|
+
userId: validation.userId || null,
|
|
299
|
+
displayName: spec.displayName,
|
|
300
|
+
lastSyncAt: null,
|
|
301
|
+
lastHealth: { ok: true, at: now },
|
|
302
|
+
cookieSpecVersion,
|
|
303
|
+
};
|
|
304
|
+
await accountsStore.put(vendor, entry);
|
|
305
|
+
|
|
306
|
+
return { ok: true, vendor, accountId, validation };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Step 4 (optional, re-login flow).
|
|
311
|
+
*
|
|
312
|
+
* Clears the vendor's partition storage and re-runs openVendorLogin so the
|
|
313
|
+
* renderer can re-host BrowserView fresh. Does NOT touch accounts.json —
|
|
314
|
+
* `registerVendor` will overwrite the entry once the user finishes.
|
|
315
|
+
*/
|
|
316
|
+
async function rotateLoginPartition({ vendor } = {}) {
|
|
317
|
+
const spec = specLookup(vendor);
|
|
318
|
+
if (!spec) {
|
|
319
|
+
return { ok: false, reason: "UNKNOWN_VENDOR", vendor };
|
|
320
|
+
}
|
|
321
|
+
if (fallbackMode !== "paste") {
|
|
322
|
+
const partition = _partitionNameFor(vendor);
|
|
323
|
+
try {
|
|
324
|
+
const session = sessionFactory(partition);
|
|
325
|
+
if (session && typeof session.clearStorageData === "function") {
|
|
326
|
+
await session.clearStorageData({ storages: ["cookies"] });
|
|
327
|
+
}
|
|
328
|
+
} catch (err) {
|
|
329
|
+
logger.warn(`rotateLoginPartition: clearStorageData failed for ${vendor}`, err);
|
|
330
|
+
// Fall through — openVendorLogin will still surface fresh state.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return openVendorLogin({ vendor });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Startup-time housekeeping.
|
|
338
|
+
*
|
|
339
|
+
* Walks known partitions and clears any whose vendor is no longer in
|
|
340
|
+
* accounts.json. Prevents Trap T8 (wizard crashed mid-login → partition
|
|
341
|
+
* cookies linger forever).
|
|
342
|
+
*
|
|
343
|
+
* Electron has no public "list partitions" API; the caller supplies the
|
|
344
|
+
* list via `partitions` (typically derived from disk scan). Tests pass a
|
|
345
|
+
* synthetic array.
|
|
346
|
+
*/
|
|
347
|
+
async function cleanupOrphanPartitions({ partitions = [] } = {}) {
|
|
348
|
+
const registered = new Set();
|
|
349
|
+
try {
|
|
350
|
+
const list = await accountsStore.list();
|
|
351
|
+
for (const entry of list || []) {
|
|
352
|
+
if (entry && entry.vendor) registered.add(entry.vendor);
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
logger.warn("cleanupOrphanPartitions: accountsStore.list failed", err);
|
|
356
|
+
// Be conservative — without accounts list we cannot safely decide,
|
|
357
|
+
// so skip the sweep.
|
|
358
|
+
return { ok: false, reason: "ACCOUNTS_LIST_FAILED", cleared: [] };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const cleared = [];
|
|
362
|
+
const known = new Set(knownVendors);
|
|
363
|
+
for (const partName of partitions) {
|
|
364
|
+
if (!_isAichatPartition(partName)) continue;
|
|
365
|
+
const vendor = _vendorFromPartition(partName);
|
|
366
|
+
if (!known.has(vendor)) continue; // Unknown vendor → not ours, leave it.
|
|
367
|
+
if (registered.has(vendor)) continue; // User has it active, keep.
|
|
368
|
+
try {
|
|
369
|
+
const session = sessionFactory(partName);
|
|
370
|
+
if (session && typeof session.clearStorageData === "function") {
|
|
371
|
+
await session.clearStorageData({ storages: ["cookies"] });
|
|
372
|
+
cleared.push(vendor);
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
logger.warn(`cleanupOrphanPartitions: clear failed ${partName}`, err);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return { ok: true, cleared };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function _existingRegisteredAt(vendor) {
|
|
382
|
+
try {
|
|
383
|
+
const e = await accountsStore.get(vendor);
|
|
384
|
+
return e && e.registeredAt ? Number(e.registeredAt) : null;
|
|
385
|
+
} catch (_err) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
openVendorLogin,
|
|
392
|
+
probeCookies,
|
|
393
|
+
registerVendor,
|
|
394
|
+
rotateLoginPartition,
|
|
395
|
+
cleanupOrphanPartitions,
|
|
396
|
+
// exported for tests / debug
|
|
397
|
+
_internal: { _partitionNameFor, _flattenJar, _projectCookies, _isAichatPartition },
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Reduce any classifier-accepted shape (object / Cookie[] / "k=v;" string)
|
|
403
|
+
* to a plain `{ name: value }` jar for persistence. Mirrors
|
|
404
|
+
* cookie-capture-spec _normalizeCookieJar but lives here so we don't reach
|
|
405
|
+
* into the adapter package's `_internal`.
|
|
406
|
+
*/
|
|
407
|
+
function _flattenJar(input) {
|
|
408
|
+
if (!input) return {};
|
|
409
|
+
if (Array.isArray(input)) {
|
|
410
|
+
const out = {};
|
|
411
|
+
for (const c of input) {
|
|
412
|
+
if (c && typeof c.name === "string" && typeof c.value === "string") {
|
|
413
|
+
out[c.name] = c.value;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
if (typeof input === "string") {
|
|
419
|
+
const out = {};
|
|
420
|
+
for (const pairRaw of input.split(/;\s*/)) {
|
|
421
|
+
const pair = pairRaw.trim();
|
|
422
|
+
if (!pair) continue;
|
|
423
|
+
const idx = pair.indexOf("=");
|
|
424
|
+
if (idx <= 0) continue;
|
|
425
|
+
out[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
if (typeof input === "object") {
|
|
430
|
+
const out = {};
|
|
431
|
+
for (const [k, v] of Object.entries(input)) {
|
|
432
|
+
if (typeof v === "string") out[k] = v;
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
return {};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Return a slimmed-down cookie projection suitable for shipping back to the
|
|
441
|
+
* renderer (no sensitive values for optional cookies — only the names).
|
|
442
|
+
*
|
|
443
|
+
* Required cookies' values are returned full-fidelity because the renderer
|
|
444
|
+
* needs them to drive Step 3 (registerVendor); the renderer is trusted with
|
|
445
|
+
* the same DOM origin as the BrowserView already saw them.
|
|
446
|
+
*/
|
|
447
|
+
function _projectCookies(classified, raw, spec) {
|
|
448
|
+
const flat = _flattenJar(raw);
|
|
449
|
+
const out = {};
|
|
450
|
+
for (const name of classified.foundRequired || []) {
|
|
451
|
+
out[name] = flat[name];
|
|
452
|
+
}
|
|
453
|
+
for (const name of classified.foundOptional || []) {
|
|
454
|
+
out[name] = flat[name];
|
|
455
|
+
}
|
|
456
|
+
// Belt-and-braces: limit to spec.required + spec.optional so unrelated
|
|
457
|
+
// cookies leaked from the paste path don't escape the wizard surface.
|
|
458
|
+
const allowed = new Set([
|
|
459
|
+
...(spec.requiredCookies || []),
|
|
460
|
+
...(spec.optionalCookies || []),
|
|
461
|
+
]);
|
|
462
|
+
const filtered = {};
|
|
463
|
+
for (const [k, v] of Object.entries(out)) {
|
|
464
|
+
if (allowed.has(k)) filtered[k] = v;
|
|
465
|
+
}
|
|
466
|
+
return filtered;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
module.exports = {
|
|
470
|
+
createAIChatWizardController,
|
|
471
|
+
PARTITION_PREFIX,
|
|
472
|
+
_internal: { _partitionNameFor, _isAichatPartition, _vendorFromPartition, _flattenJar },
|
|
473
|
+
};
|
|
@@ -224,6 +224,10 @@ class AlipayBillAdapter {
|
|
|
224
224
|
fileSha256: raw.payload.fileSha256,
|
|
225
225
|
billPeriod: raw.payload.billPeriod || undefined,
|
|
226
226
|
counterpartyKind,
|
|
227
|
+
// Phase 11 SpendingSkill + Phase 8 EntityResolver both index on
|
|
228
|
+
// extra.counterparty — surface the human-readable name here so
|
|
229
|
+
// analysis skill breakdowns group by 商家 / 转账对方 correctly.
|
|
230
|
+
counterparty: row.counterparty || undefined,
|
|
227
231
|
...(counterpartyKind === "unknown" ? { needsResolve: true } : {}),
|
|
228
232
|
},
|
|
229
233
|
};
|