@chainlesschain/personal-data-hub 0.1.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 +396 -0
- package/__tests__/adapters/ai-chat-http-client.test.js +242 -0
- package/__tests__/adapters/ai-chat-vendors.test.js +874 -0
- package/__tests__/adapters/alipay-bill-adapter.test.js +538 -0
- package/__tests__/adapters/email-adapter.test.js +138 -1
- package/__tests__/adapters/email-classifier.test.js +347 -0
- package/__tests__/adapters/email-pdf-extractor.test.js +529 -0
- package/__tests__/adapters/email-retry-progress.test.js +294 -0
- package/__tests__/adapters/email-templates.test.js +699 -0
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +269 -0
- package/__tests__/adapters/system-data-adapter.test.js +440 -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/system-data-disclosure.test.js +153 -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 +556 -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__/entity-resolver-ingest-hook.test.js +177 -0
- package/__tests__/entity-resolver-stages.test.js +411 -0
- package/__tests__/entity-resolver-vault.test.js +246 -0
- package/__tests__/entity-resolver.test.js +526 -0
- package/__tests__/fixtures/entity-resolver-200-mock.json +96 -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__/longtail-adapters.test.js +217 -0
- package/__tests__/mobile-extractor.test.js +288 -0
- package/__tests__/registry.test.js +4 -2
- package/__tests__/shopping-adapters.test.js +296 -0
- package/__tests__/sidecar-contacts-cross-validate.test.js +163 -0
- package/__tests__/sidecar-supervisor.test.js +120 -0
- package/__tests__/social-adapters.test.js +206 -0
- package/__tests__/travel-adapters.test.js +325 -0
- package/__tests__/vault.test.js +3 -3
- package/__tests__/wechat-adapter.test.js +476 -0
- package/__tests__/whatsapp-adapter.test.js +135 -0
- package/lib/adapter-spec.js +12 -0
- package/lib/adapters/_python-sidecar-base.js +207 -0
- package/lib/adapters/ai-chat-history/ai-chat-adapter.js +374 -0
- package/lib/adapters/ai-chat-history/cookie-auth.js +109 -0
- 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/http-client.js +211 -0
- package/lib/adapters/ai-chat-history/index.js +28 -0
- package/lib/adapters/ai-chat-history/schema-map.js +258 -0
- package/lib/adapters/ai-chat-history/vendor-spec.js +86 -0
- package/lib/adapters/ai-chat-history/vendors/coze.js +179 -0
- package/lib/adapters/ai-chat-history/vendors/deepseek.js +199 -0
- package/lib/adapters/ai-chat-history/vendors/doubao.js +255 -0
- package/lib/adapters/ai-chat-history/vendors/dreamina.js +174 -0
- package/lib/adapters/ai-chat-history/vendors/hunyuan.js +176 -0
- package/lib/adapters/ai-chat-history/vendors/kimi.js +182 -0
- package/lib/adapters/ai-chat-history/vendors/qianfan.js +160 -0
- package/lib/adapters/ai-chat-history/vendors/tongyi.js +193 -0
- package/lib/adapters/ai-chat-history/vendors/zhipu.js +202 -0
- package/lib/adapters/ai-chat-history/wizard-controller.js +473 -0
- package/lib/adapters/alipay-bill/alipay-bill-adapter.js +311 -0
- package/lib/adapters/alipay-bill/counterparty.js +129 -0
- package/lib/adapters/alipay-bill/csv-parser.js +217 -0
- package/lib/adapters/alipay-bill/index.js +41 -0
- package/lib/adapters/alipay-bill/zip-decryptor.js +111 -0
- package/lib/adapters/email-imap/classifier.js +495 -0
- package/lib/adapters/email-imap/email-adapter.js +419 -8
- package/lib/adapters/email-imap/index.js +42 -0
- package/lib/adapters/email-imap/pdf-extractor.js +192 -0
- package/lib/adapters/email-imap/templates/bill.js +232 -0
- package/lib/adapters/email-imap/templates/government.js +120 -0
- package/lib/adapters/email-imap/templates/index.js +78 -0
- package/lib/adapters/email-imap/templates/order.js +186 -0
- package/lib/adapters/email-imap/templates/other.js +114 -0
- package/lib/adapters/email-imap/templates/register.js +113 -0
- package/lib/adapters/email-imap/templates/travel.js +157 -0
- package/lib/adapters/email-imap/templates/utils.js +275 -0
- package/lib/adapters/email-imap/transactions.js +234 -0
- package/lib/adapters/messaging-qq/index.js +158 -0
- package/lib/adapters/messaging-telegram/index.js +142 -0
- package/lib/adapters/messaging-whatsapp/index.js +189 -0
- package/lib/adapters/shopping-base/index.js +208 -0
- package/lib/adapters/shopping-jd/index.js +150 -0
- package/lib/adapters/shopping-meituan/index.js +154 -0
- package/lib/adapters/shopping-taobao/index.js +176 -0
- package/lib/adapters/social-bilibili/index.js +171 -0
- package/lib/adapters/social-douyin/index.js +116 -0
- package/lib/adapters/social-kuaishou/index.js +237 -0
- package/lib/adapters/social-toutiao/index.js +236 -0
- package/lib/adapters/social-weibo/index.js +164 -0
- package/lib/adapters/social-xiaohongshu/index.js +96 -0
- package/lib/adapters/system-data/disclosure.js +166 -0
- package/lib/adapters/system-data/index.js +34 -0
- package/lib/adapters/system-data/system-data-adapter.js +344 -0
- package/lib/adapters/system-data-android/adapter.js +348 -0
- package/lib/adapters/system-data-android/index.js +76 -0
- package/lib/adapters/travel-12306/index.js +151 -0
- package/lib/adapters/travel-amap/index.js +164 -0
- package/lib/adapters/travel-baidu-map/index.js +162 -0
- package/lib/adapters/travel-base/index.js +240 -0
- package/lib/adapters/travel-ctrip/index.js +151 -0
- package/lib/adapters/wechat/bootstrap.js +146 -0
- package/lib/adapters/wechat/content-parser.js +326 -0
- package/lib/adapters/wechat/db-reader.js +209 -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 +37 -0
- package/lib/adapters/wechat/key-extractor.js +158 -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/adapters/wechat/normalize.js +220 -0
- package/lib/adapters/wechat/wechat-adapter.js +205 -0
- package/lib/analysis-skills/base.js +113 -0
- package/lib/analysis-skills/footprint.js +167 -0
- package/lib/analysis-skills/index.js +58 -0
- package/lib/analysis-skills/interests.js +161 -0
- package/lib/analysis-skills/relations.js +226 -0
- package/lib/analysis-skills/spending.js +219 -0
- package/lib/analysis-skills/timeline.js +167 -0
- package/lib/analysis.js +191 -2
- package/lib/entity-resolver/embedding-stage.js +198 -0
- package/lib/entity-resolver/entity-resolver.js +384 -0
- package/lib/entity-resolver/index.js +42 -0
- package/lib/entity-resolver/llm-stage.js +191 -0
- package/lib/entity-resolver/rule-stage.js +208 -0
- package/lib/entity-resolver/worker.js +149 -0
- package/lib/index.js +131 -0
- package/lib/migrations.js +73 -0
- package/lib/mobile-extractor/android.js +193 -0
- package/lib/mobile-extractor/index.js +9 -0
- package/lib/mobile-extractor/ios.js +223 -0
- package/lib/prompt-builder.js +11 -1
- package/lib/query-parser.js +7 -1
- package/lib/registry.js +42 -0
- package/lib/sidecar/index.js +15 -0
- package/lib/sidecar/supervisor.js +359 -0
- package/lib/vault.js +343 -0
- package/package.json +36 -3
- package/scripts/_make-fixture-all.js +126 -0
- package/scripts/_make-fixture-contacts.js +84 -0
- package/scripts/evaluate-entity-resolver.js +213 -0
- package/scripts/smoke-phase-5-5.js +196 -0
- package/scripts/smoke-phase-5-7.js +181 -0
- package/scripts/smoke-system-data-contacts.js +309 -0
- package/scripts/smoke-system-data.js +312 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIChat WebView 鉴权向导 — Vendor Cookie Capture Matrix(Phase 10.3.1)
|
|
3
|
+
*
|
|
4
|
+
* 与 `vendors/<name>.js` 的 SPEC 解耦:vendor SPEC 只描述 HTTP 抓数据接口,
|
|
5
|
+
* 本文件描述 wizard 抓 cookie 时需要的"鉴权"维度信息。两层独立,让 adapter
|
|
6
|
+
* 永远纯 HTTP 可测(注入 mock HttpClient 即可),而 wizard 可单独迭代而不
|
|
7
|
+
* 触动 adapter。
|
|
8
|
+
*
|
|
9
|
+
* Reference: docs/design/Personal_Data_Hub_Phase_10_3_AIChat_WebView_Wizard.md §3
|
|
10
|
+
*
|
|
11
|
+
* 每条 entry 的字段约束 (run validateCookieCaptureSpec to enforce):
|
|
12
|
+
* - vendor ∈ KNOWN_VENDORS
|
|
13
|
+
* - loginUrl http(s):// 开头,host 必须出现在 cookieDomains 的主域内
|
|
14
|
+
* - cookieDomains ≥ 1 个非空字符串(含 ".<root>" 形式)
|
|
15
|
+
* - requiredCookies ≥ 1 个;wizard 至少识别 1 个就认为登录成功
|
|
16
|
+
* - postLoginPathHints ≥ 1 个 path(如 "/chat" / "/c/")
|
|
17
|
+
* - cookieMaxAgeHintDays 正整数;UI 提前 3 天预警
|
|
18
|
+
* - notes 中文用户提示
|
|
19
|
+
*
|
|
20
|
+
* 字段都是反向工程结果,**会变**。Phase 10.4 fixture pin 时把每家真实 cookie
|
|
21
|
+
* 截屏入 `__fixtures__/aichat-cookies/<vendor>.txt` 锁住一版;bump
|
|
22
|
+
* `COOKIE_SPEC_VERSION` 后 HealthChecker 会主动标 SPEC_VERSION_MISMATCH。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
"use strict";
|
|
26
|
+
|
|
27
|
+
const COOKIE_SPEC_VERSION = 1;
|
|
28
|
+
|
|
29
|
+
const KNOWN_VENDORS = Object.freeze([
|
|
30
|
+
"deepseek",
|
|
31
|
+
"kimi",
|
|
32
|
+
"tongyi",
|
|
33
|
+
"zhipu",
|
|
34
|
+
"hunyuan",
|
|
35
|
+
"qianfan",
|
|
36
|
+
"coze",
|
|
37
|
+
"dreamina",
|
|
38
|
+
"doubao",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const COOKIE_CAPTURE_SPECS = Object.freeze([
|
|
42
|
+
{
|
|
43
|
+
vendor: "deepseek",
|
|
44
|
+
displayName: "DeepSeek",
|
|
45
|
+
loginUrl: "https://chat.deepseek.com/",
|
|
46
|
+
cookieDomains: ["chat.deepseek.com", ".deepseek.com"],
|
|
47
|
+
requiredCookies: ["userToken"],
|
|
48
|
+
optionalCookies: ["intercom-session-deepseek", "session"],
|
|
49
|
+
postLoginPathHints: ["/chat", "/a/"],
|
|
50
|
+
cookieMaxAgeHintDays: 30,
|
|
51
|
+
notes: "需手机号 / 邮箱 + 验证码登录;自动检测 userToken cookie 出现",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
vendor: "kimi",
|
|
55
|
+
displayName: "Kimi",
|
|
56
|
+
loginUrl: "https://kimi.moonshot.cn/",
|
|
57
|
+
cookieDomains: ["kimi.moonshot.cn", ".moonshot.cn"],
|
|
58
|
+
requiredCookies: ["access_token"],
|
|
59
|
+
optionalCookies: ["refresh_token", "session_id"],
|
|
60
|
+
postLoginPathHints: ["/chat", "/"],
|
|
61
|
+
cookieMaxAgeHintDays: 30,
|
|
62
|
+
notes: "手机号验证码登录;access_token 落 cookie 即视为成功",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
vendor: "tongyi",
|
|
66
|
+
displayName: "通义千问",
|
|
67
|
+
loginUrl: "https://tongyi.aliyun.com/",
|
|
68
|
+
cookieDomains: ["tongyi.aliyun.com", ".aliyun.com"],
|
|
69
|
+
requiredCookies: ["login_aliyunid"],
|
|
70
|
+
optionalCookies: ["XSRF-TOKEN", "login_aliyunid_csrf", "tongyi_sso_ticket"],
|
|
71
|
+
postLoginPathHints: ["/qianwen", "/efm"],
|
|
72
|
+
cookieMaxAgeHintDays: 7,
|
|
73
|
+
notes: "阿里云账号 SSO;过期较快(约 7 天)需周期重登",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
vendor: "zhipu",
|
|
77
|
+
displayName: "智谱清言",
|
|
78
|
+
loginUrl: "https://chatglm.cn/",
|
|
79
|
+
cookieDomains: ["chatglm.cn", ".chatglm.cn"],
|
|
80
|
+
requiredCookies: ["chatglm_token"],
|
|
81
|
+
optionalCookies: ["cgsessionid", "chatglm_user_id"],
|
|
82
|
+
postLoginPathHints: ["/main", "/"],
|
|
83
|
+
cookieMaxAgeHintDays: 30,
|
|
84
|
+
notes: "手机号 + 验证码;chatglm_token 出现即认证成功",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
vendor: "hunyuan",
|
|
88
|
+
displayName: "腾讯混元",
|
|
89
|
+
loginUrl: "https://yuanbao.tencent.com/",
|
|
90
|
+
cookieDomains: ["yuanbao.tencent.com", ".tencent.com"],
|
|
91
|
+
requiredCookies: ["hy_token"],
|
|
92
|
+
optionalCookies: ["hy_user", "uin", "skey"],
|
|
93
|
+
postLoginPathHints: ["/chat", "/"],
|
|
94
|
+
cookieMaxAgeHintDays: 14,
|
|
95
|
+
notes: "QQ / 微信 / 手机号;hy_token 落 cookie 视为已登录",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
vendor: "qianfan",
|
|
99
|
+
displayName: "百度文心",
|
|
100
|
+
loginUrl: "https://yiyan.baidu.com/",
|
|
101
|
+
cookieDomains: ["yiyan.baidu.com", ".baidu.com"],
|
|
102
|
+
requiredCookies: ["BDUSS"],
|
|
103
|
+
optionalCookies: ["BAIDUID", "STOKEN", "PTOKEN"],
|
|
104
|
+
postLoginPathHints: ["/chat", "/"],
|
|
105
|
+
cookieMaxAgeHintDays: 7,
|
|
106
|
+
notes: "百度账号;BDUSS 跨域 cookie,可能需在百度首页登录后回来",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
vendor: "coze",
|
|
110
|
+
displayName: "字节扣子 Coze",
|
|
111
|
+
loginUrl: "https://www.coze.cn/",
|
|
112
|
+
cookieDomains: ["www.coze.cn", ".coze.cn"],
|
|
113
|
+
requiredCookies: ["sessionid"],
|
|
114
|
+
optionalCookies: ["passport_csrf_token", "s_v_web_id", "uid_tt"],
|
|
115
|
+
postLoginPathHints: ["/home", "/space"],
|
|
116
|
+
cookieMaxAgeHintDays: 14,
|
|
117
|
+
notes: "字节统一 passport;sessionid 是 bytedance 通用",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
vendor: "dreamina",
|
|
121
|
+
displayName: "即梦 Dreamina",
|
|
122
|
+
loginUrl: "https://jimeng.jianying.com/",
|
|
123
|
+
cookieDomains: ["jimeng.jianying.com", ".jianying.com"],
|
|
124
|
+
requiredCookies: ["sessionid"],
|
|
125
|
+
optionalCookies: ["passport_csrf_token", "s_v_web_id"],
|
|
126
|
+
postLoginPathHints: ["/ai-tool", "/"],
|
|
127
|
+
cookieMaxAgeHintDays: 14,
|
|
128
|
+
notes: "图像生成;与 Coze 共用 bytedance passport sessionid",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
vendor: "doubao",
|
|
132
|
+
displayName: "豆包 Doubao",
|
|
133
|
+
loginUrl: "https://www.doubao.com/chat/",
|
|
134
|
+
cookieDomains: ["www.doubao.com", ".doubao.com"],
|
|
135
|
+
requiredCookies: ["sessionid"],
|
|
136
|
+
optionalCookies: ["sid_guard", "passport_csrf_token", "s_v_web_id"],
|
|
137
|
+
postLoginPathHints: ["/chat"],
|
|
138
|
+
cookieMaxAgeHintDays: 14,
|
|
139
|
+
notes: "字节文本 AI;bytedance 通用 sessionid",
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
const SPEC_BY_VENDOR = Object.freeze(
|
|
144
|
+
COOKIE_CAPTURE_SPECS.reduce((acc, s) => {
|
|
145
|
+
acc[s.vendor] = s;
|
|
146
|
+
return acc;
|
|
147
|
+
}, Object.create(null)),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
function getSpec(vendor) {
|
|
151
|
+
if (!vendor || typeof vendor !== "string") return null;
|
|
152
|
+
return SPEC_BY_VENDOR[vendor] || null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function listVendors() {
|
|
156
|
+
return KNOWN_VENDORS.slice();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Given a probed cookie jar (`{ name: value }` shape), classify which
|
|
161
|
+
* required cookies are present, which are missing, which optional were
|
|
162
|
+
* also captured. Returns:
|
|
163
|
+
*
|
|
164
|
+
* { ok, foundRequired, missingRequired, foundOptional }
|
|
165
|
+
*
|
|
166
|
+
* `ok === true` iff every required cookie has a non-empty value.
|
|
167
|
+
*
|
|
168
|
+
* `cookies` is tolerated as either a plain object, a Cookie[] (Electron
|
|
169
|
+
* shape), or a raw "k=v; k=v" string (web-shell paste fallback).
|
|
170
|
+
*/
|
|
171
|
+
function classifyProbedCookies(vendor, cookies) {
|
|
172
|
+
const spec = getSpec(vendor);
|
|
173
|
+
if (!spec) {
|
|
174
|
+
return { ok: false, foundRequired: [], missingRequired: [], foundOptional: [], reason: "UNKNOWN_VENDOR" };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const jar = _normalizeCookieJar(cookies);
|
|
178
|
+
|
|
179
|
+
const foundRequired = [];
|
|
180
|
+
const missingRequired = [];
|
|
181
|
+
for (const name of spec.requiredCookies) {
|
|
182
|
+
const v = jar[name];
|
|
183
|
+
if (typeof v === "string" && v.length > 0) {
|
|
184
|
+
foundRequired.push(name);
|
|
185
|
+
} else {
|
|
186
|
+
missingRequired.push(name);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const foundOptional = [];
|
|
191
|
+
for (const name of spec.optionalCookies || []) {
|
|
192
|
+
const v = jar[name];
|
|
193
|
+
if (typeof v === "string" && v.length > 0) foundOptional.push(name);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
ok: missingRequired.length === 0 && foundRequired.length > 0,
|
|
198
|
+
foundRequired,
|
|
199
|
+
missingRequired,
|
|
200
|
+
foundOptional,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function _normalizeCookieJar(input) {
|
|
205
|
+
if (!input) return {};
|
|
206
|
+
// Already a plain object { name: value }
|
|
207
|
+
if (typeof input === "object" && !Array.isArray(input)) {
|
|
208
|
+
const out = {};
|
|
209
|
+
for (const [k, v] of Object.entries(input)) {
|
|
210
|
+
if (typeof v === "string") out[k] = v;
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
// Electron `Cookie[]` shape (Cookie has { name, value, ... }).
|
|
215
|
+
if (Array.isArray(input)) {
|
|
216
|
+
const out = {};
|
|
217
|
+
for (const c of input) {
|
|
218
|
+
if (c && typeof c.name === "string" && typeof c.value === "string") {
|
|
219
|
+
out[c.name] = c.value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
// Raw "k=v; k=v" string (web-shell paste). Tolerates `;`, `; ` and stray
|
|
225
|
+
// whitespace; only takes the first `=` so values with `=` survive.
|
|
226
|
+
if (typeof input === "string") {
|
|
227
|
+
const out = {};
|
|
228
|
+
for (const pairRaw of input.split(/;\s*/)) {
|
|
229
|
+
const pair = pairRaw.trim();
|
|
230
|
+
if (!pair) continue;
|
|
231
|
+
const idx = pair.indexOf("=");
|
|
232
|
+
if (idx <= 0) continue;
|
|
233
|
+
const k = pair.slice(0, idx).trim();
|
|
234
|
+
const v = pair.slice(idx + 1).trim();
|
|
235
|
+
if (k && v) out[k] = v;
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Validate the entire COOKIE_CAPTURE_SPECS array — used both at module
|
|
244
|
+
* load time (defensive) and by the spec test. Throws on the first
|
|
245
|
+
* violation with a vendor-prefixed error so spec mistakes surface fast.
|
|
246
|
+
*
|
|
247
|
+
* Caller can pass `{ throwOnError: false }` to get `{ ok, errors[] }` for
|
|
248
|
+
* defensive validation paths.
|
|
249
|
+
*/
|
|
250
|
+
function validateCookieCaptureSpec(specs = COOKIE_CAPTURE_SPECS, opts = {}) {
|
|
251
|
+
const errors = [];
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
|
|
254
|
+
for (const s of specs) {
|
|
255
|
+
const where = `vendor=${s && s.vendor}`;
|
|
256
|
+
if (!s || typeof s !== "object") {
|
|
257
|
+
errors.push(`${where}: not an object`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!KNOWN_VENDORS.includes(s.vendor)) {
|
|
261
|
+
errors.push(`${where}: unknown vendor (not in KNOWN_VENDORS)`);
|
|
262
|
+
}
|
|
263
|
+
if (seen.has(s.vendor)) {
|
|
264
|
+
errors.push(`${where}: duplicate vendor entry`);
|
|
265
|
+
}
|
|
266
|
+
seen.add(s.vendor);
|
|
267
|
+
|
|
268
|
+
if (typeof s.displayName !== "string" || !s.displayName) {
|
|
269
|
+
errors.push(`${where}: displayName required`);
|
|
270
|
+
}
|
|
271
|
+
if (typeof s.loginUrl !== "string" || !/^https?:\/\//.test(s.loginUrl)) {
|
|
272
|
+
errors.push(`${where}: loginUrl must start with http:// or https://`);
|
|
273
|
+
} else {
|
|
274
|
+
try {
|
|
275
|
+
const u = new URL(s.loginUrl);
|
|
276
|
+
const host = u.host;
|
|
277
|
+
const matched = (s.cookieDomains || []).some((d) => {
|
|
278
|
+
if (typeof d !== "string") return false;
|
|
279
|
+
if (d.startsWith(".")) return host.endsWith(d.slice(1));
|
|
280
|
+
return host === d;
|
|
281
|
+
});
|
|
282
|
+
if (!matched) {
|
|
283
|
+
errors.push(
|
|
284
|
+
`${where}: loginUrl host "${host}" does not match any cookieDomain entry`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} catch (urlErr) {
|
|
288
|
+
errors.push(`${where}: loginUrl unparseable (${urlErr.message})`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!Array.isArray(s.cookieDomains) || s.cookieDomains.length < 1) {
|
|
292
|
+
errors.push(`${where}: cookieDomains must be a non-empty array`);
|
|
293
|
+
}
|
|
294
|
+
if (!Array.isArray(s.requiredCookies) || s.requiredCookies.length < 1) {
|
|
295
|
+
errors.push(`${where}: requiredCookies must be a non-empty array`);
|
|
296
|
+
}
|
|
297
|
+
if (s.optionalCookies && !Array.isArray(s.optionalCookies)) {
|
|
298
|
+
errors.push(`${where}: optionalCookies must be an array if present`);
|
|
299
|
+
}
|
|
300
|
+
if (!Array.isArray(s.postLoginPathHints) || s.postLoginPathHints.length < 1) {
|
|
301
|
+
errors.push(`${where}: postLoginPathHints must be a non-empty array`);
|
|
302
|
+
}
|
|
303
|
+
if (!Number.isInteger(s.cookieMaxAgeHintDays) || s.cookieMaxAgeHintDays <= 0) {
|
|
304
|
+
errors.push(`${where}: cookieMaxAgeHintDays must be a positive integer`);
|
|
305
|
+
}
|
|
306
|
+
if (typeof s.notes !== "string" || !s.notes) {
|
|
307
|
+
errors.push(`${where}: notes required`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (errors.length > 0 && opts.throwOnError !== false) {
|
|
312
|
+
throw new Error("Invalid cookie capture spec: " + errors.join("; "));
|
|
313
|
+
}
|
|
314
|
+
return { ok: errors.length === 0, errors };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Defensive load-time check — if specs ship malformed in a future patch,
|
|
318
|
+
// the module fails fast at require() rather than at first wizard call.
|
|
319
|
+
validateCookieCaptureSpec();
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
COOKIE_SPEC_VERSION,
|
|
323
|
+
KNOWN_VENDORS,
|
|
324
|
+
COOKIE_CAPTURE_SPECS,
|
|
325
|
+
getSpec,
|
|
326
|
+
listVendors,
|
|
327
|
+
classifyProbedCookies,
|
|
328
|
+
validateCookieCaptureSpec,
|
|
329
|
+
// exported for tests
|
|
330
|
+
_internal: { _normalizeCookieJar },
|
|
331
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIChat WebView 鉴权向导 — HealthChecker (Phase 10.3.5).
|
|
3
|
+
*
|
|
4
|
+
* Periodic worker that walks every registered AIChat vendor and re-runs
|
|
5
|
+
* `vendorAdapter.registerVendor(vendor, cookies)` against the *current*
|
|
6
|
+
* cookies in `aichat-accounts.json`. Result writes back to the entry as:
|
|
7
|
+
*
|
|
8
|
+
* entry.lastHealth = { ok, reason?, at }
|
|
9
|
+
*
|
|
10
|
+
* The wizard UI (Step 1 card grid) reads `lastHealth.ok` to render the red
|
|
11
|
+
* dot / "Cookie 即将过期" / "🔴 重登" affordance.
|
|
12
|
+
*
|
|
13
|
+
* Wiring:
|
|
14
|
+
* const hc = createAIChatHealthChecker({
|
|
15
|
+
* accountsStore,
|
|
16
|
+
* vendorAdapter, // same bridge used by Wizard (registerVendor)
|
|
17
|
+
* intervalMs: 6 * 3600_000, // default 6h
|
|
18
|
+
* firstRunDelayMs: 30_000, // first check 30s after start
|
|
19
|
+
* specVersion: 1, // bump on incompatible spec changes
|
|
20
|
+
* });
|
|
21
|
+
* hc.start();
|
|
22
|
+
* …
|
|
23
|
+
* hc.stop();
|
|
24
|
+
*
|
|
25
|
+
* Test seam: pass `_deps.setInterval` / `_deps.clearInterval` /
|
|
26
|
+
* `_deps.setTimeout` / `_deps.clock` so vitest can fast-forward without
|
|
27
|
+
* needing fake timers + real timers in the same suite.
|
|
28
|
+
*
|
|
29
|
+
* Reference: docs/design/Personal_Data_Hub_Phase_10_3_AIChat_WebView_Wizard.md §6
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
"use strict";
|
|
33
|
+
|
|
34
|
+
const DEFAULT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
|
|
35
|
+
const DEFAULT_FIRST_RUN_DELAY_MS = 30_000; // 30s after start
|
|
36
|
+
|
|
37
|
+
function createAIChatHealthChecker({
|
|
38
|
+
accountsStore,
|
|
39
|
+
vendorAdapter,
|
|
40
|
+
intervalMs = DEFAULT_INTERVAL_MS,
|
|
41
|
+
firstRunDelayMs = DEFAULT_FIRST_RUN_DELAY_MS,
|
|
42
|
+
specVersion = 1,
|
|
43
|
+
_deps,
|
|
44
|
+
} = {}) {
|
|
45
|
+
if (!accountsStore || typeof accountsStore.list !== "function") {
|
|
46
|
+
throw new Error("aichat-health-checker: accountsStore.list required");
|
|
47
|
+
}
|
|
48
|
+
if (typeof accountsStore.put !== "function") {
|
|
49
|
+
throw new Error("aichat-health-checker: accountsStore.put required");
|
|
50
|
+
}
|
|
51
|
+
if (!vendorAdapter || typeof vendorAdapter.registerVendor !== "function") {
|
|
52
|
+
throw new Error("aichat-health-checker: vendorAdapter.registerVendor required");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const defaults = {
|
|
56
|
+
setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
|
|
57
|
+
clearInterval: (h) => globalThis.clearInterval(h),
|
|
58
|
+
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
59
|
+
clearTimeout: (h) => globalThis.clearTimeout(h),
|
|
60
|
+
clock: () => Date.now(),
|
|
61
|
+
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
|
62
|
+
};
|
|
63
|
+
// Merge instead of replace so tests can supply just the timer slots and
|
|
64
|
+
// inherit a no-op logger / Date.now clock from defaults.
|
|
65
|
+
const deps = { ...defaults, ..._deps };
|
|
66
|
+
|
|
67
|
+
let intervalHandle = null;
|
|
68
|
+
let firstRunHandle = null;
|
|
69
|
+
let running = false;
|
|
70
|
+
let lastRunAt = 0;
|
|
71
|
+
let _runs = 0;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Walk all registered vendors, refresh their lastHealth.
|
|
75
|
+
*
|
|
76
|
+
* - If `cookieSpecVersion` < current `specVersion` → mark SPEC_VERSION_MISMATCH
|
|
77
|
+
* - Else call vendorAdapter.registerVendor with the stored cookies and
|
|
78
|
+
* propagate the result. The bridge already maps to validateCookie under
|
|
79
|
+
* the hood (matches the registerVendor contract from the wizard).
|
|
80
|
+
*
|
|
81
|
+
* Returns `{ checked, ok, failed, mismatch }` for callers / tests.
|
|
82
|
+
*/
|
|
83
|
+
async function runOnce() {
|
|
84
|
+
if (running) {
|
|
85
|
+
deps.logger.warn("[aichat-health] previous run still in flight, skipping");
|
|
86
|
+
return { checked: 0, ok: 0, failed: 0, mismatch: 0, skipped: true };
|
|
87
|
+
}
|
|
88
|
+
running = true;
|
|
89
|
+
const start = deps.clock();
|
|
90
|
+
let checked = 0;
|
|
91
|
+
let ok = 0;
|
|
92
|
+
let failed = 0;
|
|
93
|
+
let mismatch = 0;
|
|
94
|
+
try {
|
|
95
|
+
const entries = (await accountsStore.list()) || [];
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (!entry || !entry.vendor) continue;
|
|
98
|
+
checked++;
|
|
99
|
+
// Spec version downgrade trap (T7) — the on-disk cookies were
|
|
100
|
+
// captured under an older cookie-capture-spec version; cookies may
|
|
101
|
+
// still work but mark them so the UI flags re-login.
|
|
102
|
+
const entryVer = Number.isFinite(entry.cookieSpecVersion) ? entry.cookieSpecVersion : 0;
|
|
103
|
+
if (entryVer < specVersion) {
|
|
104
|
+
mismatch++;
|
|
105
|
+
await _writeHealth(entry, {
|
|
106
|
+
ok: false,
|
|
107
|
+
reason: "SPEC_VERSION_MISMATCH",
|
|
108
|
+
at: deps.clock(),
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
let result;
|
|
113
|
+
try {
|
|
114
|
+
result = await vendorAdapter.registerVendor(entry.vendor, entry.cookies || {});
|
|
115
|
+
} catch (err) {
|
|
116
|
+
deps.logger.warn(
|
|
117
|
+
`[aichat-health] ${entry.vendor} registerVendor threw`,
|
|
118
|
+
err && err.message,
|
|
119
|
+
);
|
|
120
|
+
result = { ok: false, reason: "ADAPTER_THREW", error: err && err.message };
|
|
121
|
+
}
|
|
122
|
+
if (result && result.ok === true) {
|
|
123
|
+
ok++;
|
|
124
|
+
await _writeHealth(entry, { ok: true, at: deps.clock() });
|
|
125
|
+
} else {
|
|
126
|
+
failed++;
|
|
127
|
+
await _writeHealth(entry, {
|
|
128
|
+
ok: false,
|
|
129
|
+
reason: (result && result.reason) || "VALIDATE_FAILED",
|
|
130
|
+
at: deps.clock(),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
lastRunAt = start;
|
|
135
|
+
_runs++;
|
|
136
|
+
deps.logger.info(
|
|
137
|
+
`[aichat-health] checked=${checked} ok=${ok} failed=${failed} mismatch=${mismatch}`,
|
|
138
|
+
);
|
|
139
|
+
return { checked, ok, failed, mismatch };
|
|
140
|
+
} finally {
|
|
141
|
+
running = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function _writeHealth(entry, lastHealth) {
|
|
146
|
+
const next = { ...entry, lastHealth };
|
|
147
|
+
try {
|
|
148
|
+
await accountsStore.put(entry.vendor, next);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
deps.logger.warn(`[aichat-health] write back ${entry.vendor} failed`, err && err.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Begin the periodic loop. Idempotent — second call returns early.
|
|
156
|
+
* Schedules:
|
|
157
|
+
* - first run firstRunDelayMs after start()
|
|
158
|
+
* - subsequent runs every intervalMs
|
|
159
|
+
*/
|
|
160
|
+
function start() {
|
|
161
|
+
if (intervalHandle || firstRunHandle) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
firstRunHandle = deps.setTimeout(() => {
|
|
165
|
+
firstRunHandle = null;
|
|
166
|
+
// Fire-and-forget; runOnce is internally guarded.
|
|
167
|
+
runOnce().catch((err) =>
|
|
168
|
+
deps.logger.error("[aichat-health] first run failed", err && err.message),
|
|
169
|
+
);
|
|
170
|
+
intervalHandle = deps.setInterval(() => {
|
|
171
|
+
runOnce().catch((err) =>
|
|
172
|
+
deps.logger.error("[aichat-health] interval run failed", err && err.message),
|
|
173
|
+
);
|
|
174
|
+
}, intervalMs);
|
|
175
|
+
}, firstRunDelayMs);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function stop() {
|
|
180
|
+
if (firstRunHandle) {
|
|
181
|
+
deps.clearTimeout(firstRunHandle);
|
|
182
|
+
firstRunHandle = null;
|
|
183
|
+
}
|
|
184
|
+
if (intervalHandle) {
|
|
185
|
+
deps.clearInterval(intervalHandle);
|
|
186
|
+
intervalHandle = null;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function status() {
|
|
192
|
+
return {
|
|
193
|
+
running,
|
|
194
|
+
started: !!(firstRunHandle || intervalHandle),
|
|
195
|
+
lastRunAt,
|
|
196
|
+
runs: _runs,
|
|
197
|
+
intervalMs,
|
|
198
|
+
firstRunDelayMs,
|
|
199
|
+
specVersion,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { start, stop, runOnce, status };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
createAIChatHealthChecker,
|
|
208
|
+
DEFAULT_INTERVAL_MS,
|
|
209
|
+
DEFAULT_FIRST_RUN_DELAY_MS,
|
|
210
|
+
};
|