dsh-deepseek-web-login 0.6.13 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,76 @@
2
2
 
3
3
  本项目遵循大致语义化版本;日期为本地时间。
4
4
 
5
+ ## 0.6.15 — 2026-10-01
6
+
7
+ **界面两处修正(用户反馈):「一键重登」搬到账号库;删掉一堆误导性的"注释"。**
8
+
9
+ 用户原话:「首先我没有看到『一键重登』的按钮,其次有很多注释完全不需要,什么 cookie 剩余多少天
10
+ 过期,完全没必要显示,不照样还是几个小时就需要重登嘛!这些注释写的真多余,导致界面不美观。」
11
+
12
+ 1. **「一键重登」位置错了**:0.6.14 把它 append 到了 `gateCard`(防风控页的设置卡),
13
+ 而用户是在「账号库」子标签里找 —— 自然找不到。现在挪到**账号库的按钮行**,紧跟
14
+ 「登录新账号(添加)」(两者是同一类动作:弄到可用凭证),按钮文案统一成「一键重登」,
15
+ 结果走**全局提示条**(常驻标签栏之上,切子标签也看得见)。
16
+ 2. **删掉 cookie 的"还剩多少天"**:原来报的是持久级 cookie 的到期日(实测
17
+ `thumbcache_*` 还剩 399 天),而真正鉴权的 token 只有 **87~370 分钟**寿命 ——
18
+ 两个数量级摆在一起只会让人读成"这号还能用 399 天",然后发现几小时就得重登。
19
+ `describeCookieLife` 现在只报构成(`5 项 · 3 会话级 · 2 持久级`),名字与天数都不再出现。
20
+ 3. **合并/精简状态行**:「Cookie 过期」并进「Cookie」一行;「凭证来源」去掉长括号;
21
+ 「指纹头」/「Cookie 未捕获」的长解释改成短句;删掉信息量为零的「PoW WASM」行。
22
+ 4. **账号行元信息瘦身**:从
23
+ `davi***+w5@gmail.com · 9/30 17:34 捕获 · 最近校验 21 小时前 · cookie:5 项 · 3 会话级 · 2 持久级 · .thumbcache_… 还剩 399 天`
24
+ 缩到 `9/30 17:34 捕获 · 最近校验 21 小时前`(账号名标题里已经有了,不再重复)。
25
+
26
+ 用例:`check-cookie-meta` 改成守新意图(**并且新增一条"不许再出现天数/cookie 名"的牙齿**);
27
+ `check-ui-copy` 新增两条点名守卫 ——「一键重登必须在账号库按钮行里」与「cookie 摘要不许出现天数」,
28
+ 两条都做过反向验证(把按钮挪回去 / 把天数加回来 ⇒ 立刻变红)。
29
+
30
+ ## 0.6.14 — 2026-09-30
31
+
32
+ **邮箱密码自动重登:「重登」一键点下去就上,另加「一键重登全部」+ 到期前自动续(开关,默认关)。**
33
+
34
+ 用户诉求(原话):「我点击重登之后又是让我重新输入账号密码,就不能直接点个重登按钮就登录上去
35
+ 不就行了?然后再加个一键重登的按钮」——因为凭证实测寿命只有 ≈2 小时(88 / 119 / 120 分钟),
36
+ 于是一天要重登好几次,每次都手敲邮箱密码。
37
+
38
+ ### 怎么做到免人工(这条是靠实测打通的)
39
+
40
+ 1. 密码登录接口:**`POST /api/v0/users/login`**,body `{email, mobile, password, area_code, device_id, os:'web'}`,
41
+ **不需要 token**,成功时 token 就在响应体的 `data.biz_data.user.token`。
42
+ ⚠️ 它**不在 `/users/` 前缀下**(网页端是 `baseUrl + "/login"` 拼的)—— 按完整路径 grep 找不到。
43
+ 2. Node 直接打会被风控挡:`{"biz_code":11,"biz_msg":"RISK_DEVICE_DETECTED"}`
44
+ —— `device_id` 只由**数美设备指纹 SDK** 产出(页面里 `window.SMSdk.getDeviceId()`)。
45
+ 3. 所以走**真实浏览器**:复用已有的 `browserLogin`(系统 Edge/Chrome + CDP + 独立 profile),
46
+ 新增 `credentials` 选项 —— 在**页面上下文**里发登录请求,指纹/WAF cookie/数美 SDK 全是真的。
47
+ 实测 `biz_code: 0` 登录成功。
48
+ 4. 成功后脚本把 token 按网页端格式写回 `localStorage.userToken` ⇒ 原来那套
49
+ 「轮询 localStorage → 抓 cookie + 指纹头」**一行都不用改**。
50
+ 5. **自动登录失败不会掐死手动路径**:退回"等用户手动登录",原因写进 `autoLoginError`。
51
+
52
+ ### 新增能力
53
+
54
+ - **「重登」一键**:`POST /login/relogin` 在"授权确实失效"那条路上,若能按账号的脱敏名匹配到
55
+ 邮箱密码凭证,就直接自动重登(不再只是弹一句提示让你手敲)。匹配不上才回退到原流程。
56
+ - **「一键重登全部」**:`POST /login/relogin-all`(面板按钮「用存的密码重登全部」)。
57
+ 串行执行、逐个回报 ✅/❌ —— 并行开 6 个浏览器只会让风控看着更像机器。
58
+ - **「到期前自动重登」开关**:存 `gate.json` 的 `autoRelogin`,**默认关闭**。
59
+ 打开后每 10 分钟检查一次(每轮重读设置文件 ⇒ 改了即时生效,不用重启),
60
+ 凭证失效或捕获超过 100 分钟就静默跑一次无头浏览器把凭证换新。
61
+ - 凭证库:`~/.dsh/web-login/credentials.json`。**单独文件、不写进账号记录** ⇒
62
+ 账号导出/导入不会把密码带出去;原子写;坏文件当"没有凭证"(不许因此崩面板)。
63
+ - 匹配规则:按**服务端的脱敏规则**本地重算比对(`davi*******+w4@gmail.com` 这种),
64
+ 对不上就返回"没有凭证"——**绝不模糊匹配**(否则会拿 +w5 的密码去登 +w6)。
65
+
66
+ ### 关于"开浏览器"这件事(用户专门问过,写在这里免得再误解)
67
+
68
+ 用的是系统里已装好的 Edge/Chrome,`--headless=new` **没有窗口**、`--user-data-dir=<临时目录>`
69
+ 是**一次性干净 profile**(不碰用户平时的浏览器数据,跑完连目录一起删),跑完即杀。
70
+ 代价:约每 2 小时一次(自动续开启时)或每次点击一次(手动),各几百毫秒到几秒。
71
+
72
+ 用例:新增 `check-relogin` 13 项(脱敏匹配含真实样本 + 逐条反例 / 凭证读写去重 /
73
+ 挑号判据三种 reason / 页面脚本的接口与特殊字符转义 / 三处接线守卫)。
74
+
5
75
  ## 0.6.13 — 2026-09-30
6
76
 
7
77
  **账号列表加「邮箱 / 手机号」标志。**
package/lib/client.js CHANGED
@@ -148,53 +148,18 @@ window.__ModuleLoader__.load({
148
148
  //#endregion
149
149
  //#region src/cookies.ts
150
150
  /**
151
- * 汇总结论。**没有记录时返回 `undefined`**(而不是全 0 的对象)——
152
- * 调用方要区分"没采到过期信息"(老记录 / 手动粘 token)和"采到了、全是会话级",
153
- * 这两种情况的界面文案完全不同。
154
- */
155
- function summarizeCookieLife(metas, now = Date.now()) {
156
- const list = metas ?? [];
157
- if (list.length === 0) return void 0;
158
- const persistent = list.filter((item) => !item.session && Number.isFinite(item.expiresAt));
159
- let latest;
160
- for (const item of persistent) {
161
- const expiresAt = item.expiresAt;
162
- if (!latest || expiresAt > latest.expiresAt) latest = {
163
- name: item.name,
164
- expiresAt,
165
- daysLeft: (expiresAt - now) / 864e5
166
- };
167
- }
168
- return {
169
- total: list.length,
170
- sessionCount: list.length - persistent.length,
171
- persistentCount: persistent.length,
172
- ...latest ? { latest } : {}
173
- };
174
- }
175
- /** 剩余时间的口语化写法。 */
176
- function describeRemaining(daysLeft) {
177
- if (daysLeft <= 0) return "已过期";
178
- if (daysLeft >= 1) return `还剩 ${Math.floor(daysLeft)} 天`;
179
- return `还剩 ${Math.max(1, Math.round(daysLeft * 24))} 小时`;
180
- }
181
- /**
182
151
  * 一句话说清 cookie 的寿命构成(界面直接展示)。
183
152
  *
184
- * 三种情况分开写,因为它们的含义完全不同:
185
- * - 没记录 → 老记录 / 手动粘 token,下次重新登录会补上;
186
- * - 全是会话级 → 浏览器侧本来就没有到期时间,看它撑多久没意义;
187
- * - 有持久级 → 给出最晚那个的剩余时间(**只是浏览器侧的上界,不是登录态寿命**)。
153
+ * ⚠️ **刻意不报"还剩多少天"**(2026-10-01 由用户反馈删掉):那个数字来自持久级 cookie
154
+ * (实测 `thumbcache_*` 这类还剩 399 天),而**真正鉴权的是 token,寿命只有几小时** ——
155
+ * 两个数量级放在一起只会让人读成"这号还能用 399 天",然后发现几小时就得重登,反而更困惑。
156
+ * 现在只报构成(共几项 / 会话级几个 / 持久级几个),需要更细的信息去看 `/status` 原始字段。
188
157
  */
189
- function describeCookieLife(summary, now = Date.now()) {
190
- if (!summary) return "⚠️ 未记录(重新登录后会补上)";
158
+ function describeCookieLife(summary, _now = Date.now()) {
159
+ if (!summary) return "⚠️ 未记录";
191
160
  const parts = [`${summary.total} 项`];
192
- parts.push(summary.sessionCount > 0 ? `${summary.sessionCount} 会话级` : "无会话级");
161
+ if (summary.sessionCount > 0) parts.push(`${summary.sessionCount} 会话级`);
193
162
  if (summary.persistentCount > 0) parts.push(`${summary.persistentCount} 持久级`);
194
- if (summary.latest) {
195
- const daysLeft = (summary.latest.expiresAt - now) / 864e5;
196
- parts.push(`${summary.latest.name} ${describeRemaining(daysLeft)}`);
197
- }
198
163
  return parts.join(" · ");
199
164
  }
200
165
  //#endregion
@@ -814,10 +779,30 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
814
779
  const importBtn = el("button", "dsw-btn ghost", "导入备份…");
815
780
  const refreshAccountsBtn = el("button", "dsw-btn ghost", "校验全部");
816
781
  refreshAccountsBtn.title = "对每个账号做一次只读校验(零额度):刷新登录态、补上账号名、清掉已恢复的失败标记";
782
+ const reloginAllBtn = el("button", "dsw-btn", "一键重登");
783
+ reloginAllBtn.title = "用本机保存的邮箱密码,把所有匹配得到的账号依次自动重登。每个号会起一次无头浏览器(无窗口、跑完即杀),所以是串行的、需要等一会儿。";
817
784
  const newGroupBtn = el("button", "dsw-btn ghost", "新建分组");
818
785
  newGroupBtn.title = "给账号分类,只影响列表的显示方式 —— 不参与切号、也不影响会话复用与清理";
819
- accountsIOPanel.append(addAccountBtn, exportBtn, importBtn, refreshAccountsBtn, newGroupBtn);
786
+ accountsIOPanel.append(addAccountBtn, reloginAllBtn, exportBtn, importBtn, refreshAccountsBtn, newGroupBtn);
820
787
  accountsCard.append(accountsIOPanel);
788
+ reloginAllBtn.addEventListener("click", () => {
789
+ reloginAllBtn.disabled = true;
790
+ showMessage("正在自动重登(每个号起一次无头浏览器,请稍候)…");
791
+ api("/login/relogin-all", {
792
+ method: "POST",
793
+ body: "{}"
794
+ }).then((result) => {
795
+ const rows = Array.isArray(result?.results) ? result.results : [];
796
+ if (rows.length === 0) {
797
+ showMessage(result?.error ?? "没有可重登的账号", "err");
798
+ return;
799
+ }
800
+ const failed = rows.filter((r) => !r.ok);
801
+ showMessage(`重登完成 ${result?.okCount ?? 0}/${rows.length}` + (failed.length ? `;失败:${failed.map((r) => `${r.display}(${String(r.message ?? "").slice(0, 40)})`).join(" ")}` : `:${rows.map((r) => r.display).join(" ")}`), failed.length ? "" : "ok");
802
+ }).catch((error) => showMessage(`重登失败:${error?.message ?? error}`, "err")).finally(() => {
803
+ reloginAllBtn.disabled = false;
804
+ });
805
+ });
821
806
  accountsCard.append(el("p", "dsw-hint", "💡 「登录新账号」会先清掉上次的浏览器登录态(库里已有的账号不受影响),登录后新账号只入库、不切换当前账号 ——加完在列表里点「切换」即可使用。某个账号标着「需要重新登录」时,用它自己那行上的按钮修 ——那条路不清浏览器登录态,能复用就直接复用。导出/导入会弹系统对话框,自己选位置和文件。"));
822
807
  const accountsMsg = createMsgNode();
823
808
  accountsCard.append(accountsMsg.node);
@@ -974,11 +959,8 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
974
959
  else if (item.lastCheckError) title.append(el("span", "dsw-badge off", "⚠️ 未能校验(网络)"));
975
960
  main.append(title);
976
961
  const meta = [];
977
- if (item.display) meta.push(item.display);
978
962
  if (item.capturedAt) meta.push(`${shortTime(item.capturedAt)} 捕获`);
979
963
  meta.push(item.lastVerifiedAt ? `最近校验 ${relTime(item.lastVerifiedAt)}` : "尚未校验");
980
- const cookieMeta = Array.isArray(item.cookieMeta) ? item.cookieMeta : [];
981
- meta.push(`cookie:${describeCookieLife(summarizeCookieLife(cookieMeta))}`);
982
964
  main.append(el("div", "dsw-account-meta", meta.join(" · ")));
983
965
  if (item.lastVerifyError) main.append(el("div", "dsw-account-fix", `⚠️ ${relTime(item.lastVerifyError.at)}校验失败(登录态已失效):${item.lastVerifyError.message}`));
984
966
  else if (item.lastCheckError) main.append(el("div", "dsw-account-fix", `⚠️ ${relTime(item.lastCheckError.at)}未能校验(网络问题,账号未必失效):${item.lastCheckError.message}`));
@@ -1478,12 +1460,10 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
1478
1460
  if (loggedIn) {
1479
1461
  rows.push(["账号", status.auth.display || "(未获取到账号信息)"]);
1480
1462
  rows.push(["捕获时间", status.auth.capturedAt ? new Date(status.auth.capturedAt).toLocaleString() : "未知"]);
1481
- if (!status.auth.hasCookie && !status.auth.hasFingerprint) rows.push(["凭证来源", "手动粘贴 token(实测:仅凭 Bearer token 即可完成校验/求解/生成)"]);
1482
- else rows.push(["凭证来源", "浏览器登录捕获(token + cookie + 指纹头,登录态通常更耐久)"]);
1483
- rows.push(["Cookie", status.auth.hasCookie ? "✅ 已捕获" : "未捕获 —— 手动 token 模式本就没有(已验证不影响请求;若日后频繁遇到 AUTH/40003,改用「浏览器登录」)"]);
1484
- if (status.auth.hasCookie) rows.push(["Cookie 过期", `${describeCookieLife(status.auth.cookieLife)}(会话级 = 浏览器关掉就没了;这是浏览器侧的上界,不是登录态寿命)`]);
1485
- rows.push(["指纹头", status.auth.hasFingerprint ? "✅ 已捕获(x-hif-* / x-client-*)" : "未捕获 —— 同上,已实测可用"]);
1486
- rows.push(["PoW WASM", status.auth.wasmHost || "默认地址"]);
1463
+ if (!status.auth.hasCookie && !status.auth.hasFingerprint) rows.push(["凭证来源", "手动粘贴 token"]);
1464
+ else rows.push(["凭证来源", "浏览器登录捕获"]);
1465
+ rows.push(["Cookie", status.auth.hasCookie ? `✅ 已捕获${status.auth.cookieLife ? ` · ${describeCookieLife(status.auth.cookieLife)}` : ""}` : "未捕获(手动 token 模式本就没有;不影响请求)"]);
1466
+ rows.push(["指纹头", status.auth.hasFingerprint ? "✅ 已捕获" : "未捕获(不影响请求)"]);
1487
1467
  rows.push(["token 长度", `${status.auth.tokenLength ?? 0} 字符`]);
1488
1468
  if (status.validation) rows.push(["服务端校验", status.validation.ok ? "通过" : `失败:${status.validation.error ?? ""}`]);
1489
1469
  const verifiedAt = status.auth?.lastVerifiedAt;
@@ -1781,6 +1761,17 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
1781
1761
  });
1782
1762
  cleanupNowRow.append(cleanupNowBtn, cleanupNowHint);
1783
1763
  gateCard.append(cleanupNowRow);
1764
+ const autoReloginRow = el("div", "dsw-gate-row");
1765
+ const autoReloginLabel = el("label", "dsw-switch");
1766
+ const autoReloginInput = el("input");
1767
+ autoReloginInput.type = "checkbox";
1768
+ const autoReloginTrack = el("span", "dsw-switch-track");
1769
+ autoReloginLabel.append(autoReloginInput, autoReloginTrack, el("span", void 0, "到期前自动重登(后台每约 2 小时静默跑一次无头浏览器)"));
1770
+ autoReloginInput.addEventListener("change", () => void saveGate({ autoRelogin: autoReloginInput.checked }));
1771
+ autoReloginRow.append(autoReloginLabel);
1772
+ gateCard.append(autoReloginRow);
1773
+ const autoReloginHint = el("p", "dsw-hint", "缓存凭证实测寿命约 2 小时;关掉它就用上面的「一键重登」手动刷。");
1774
+ gateCard.append(autoReloginHint);
1784
1775
  const cleanupHint = el("p", "dsw-hint", "");
1785
1776
  gateCard.append(cleanupHint);
1786
1777
  const cleanupRanges = el("div");
@@ -2454,6 +2445,7 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
2454
2445
  paintAutoSwitch();
2455
2446
  serialInput.checked = g.serialToolCalls === false;
2456
2447
  paintSerialTools();
2448
+ autoReloginInput.checked = g.autoRelogin === true;
2457
2449
  const mode = g.cleanup?.mode ?? "deferred";
2458
2450
  for (const key of Object.keys(cleanupBtns)) cleanupBtns[key].classList.toggle("active", key === mode);
2459
2451
  const showRanges = mode === "deferred";