dsh-deepseek-web-login 0.6.29 → 0.6.30
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 +47 -0
- package/lib/index.js +86 -33
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
本项目大致遵循语义化版本;日期为本地时间。
|
|
4
4
|
|
|
5
|
+
## 0.6.30 — 2026-10-02
|
|
6
|
+
|
|
7
|
+
**补齐请求头:把浏览器**自动加**的那批指纹头收回来,并修掉一个过时的写死版本号。**
|
|
8
|
+
|
|
9
|
+
起因是调研"能不能不打开浏览器也像真浏览器发消息"(报告见工作区 `浏览器指纹伪装可行性调研.md`)。
|
|
10
|
+
调研的结论之一是:**在改 TLS 之前,头部这一层就有零成本的差距可以补**。
|
|
11
|
+
|
|
12
|
+
对着**真实登录捕获**(`accounts/acc_9be659e4.json`,2026-10-02T03:42Z)核对出两处:
|
|
13
|
+
|
|
14
|
+
1. **收头的规则只认 `x-*`,于是浏览器自动加的那批全被丢掉**:
|
|
15
|
+
`sec-ch-ua` / `sec-ch-ua-mobile` / `sec-ch-ua-platform`、
|
|
16
|
+
`sec-fetch-dest` / `sec-fetch-mode` / `sec-fetch-site`、`priority`、`accept`。
|
|
17
|
+
这批是"是不是真浏览器"最表层的信号 —— 真实 Chrome/Edge 发 fetch 请求时一定会带,
|
|
18
|
+
而我们一条都没发。现在收进来,并且**保留浏览器给头的顺序**(顺序本身就是指纹)。
|
|
19
|
+
|
|
20
|
+
2. **`x-client-version` 写死 `2.0.0`,而真实捕获是 `2.5.0`** —— 落后两个小版本,而且
|
|
21
|
+
**没有任何机制会发现它过时**。真实值一直走的是抓取那条路(所以线上没暴露),
|
|
22
|
+
但抓取失败时就会发这个陈旧值。现在:抓来的真值永远优先,写死的只做兜底并标注要定期校准。
|
|
23
|
+
|
|
24
|
+
**顺带(同一批证据的两件事):**
|
|
25
|
+
|
|
26
|
+
- `pickExtraHeaders` 的规则原先在 `browser-login.ts`(CDP 路径)和 `login.ts`(旧 webRequest 路径)
|
|
27
|
+
**各写了一遍** —— 两份一旦漂移,"同一账号换个登录方式指纹就不一样",且不会有任何报错。
|
|
28
|
+
现在只有一处,两边都调它。
|
|
29
|
+
- `captureDefect()` 增加一条判据:**抓到了头、但里面没有 `x-device-id`** 也算捕获缺陷。
|
|
30
|
+
依据:真实捕获里 `x-device-id` 是数美设备指纹(UUID),而上游对"缺少浏览器设备指纹"
|
|
31
|
+
会直接返回 `biz_code=11 / RISK_DEVICE_DETECTED` —— 缺了它不是"信息少一点",是**会被风控判定**。
|
|
32
|
+
⚠️ 只覆盖"有头但缺这一项";`extraHeaders` 整个为空的手工粘 token 路径**故意不碰**(既有设计)。
|
|
33
|
+
|
|
34
|
+
**刻意不做的一件事:不收 `accept-encoding`。**
|
|
35
|
+
它是浏览器的解压能力声明,服务端可能据此回 `zstd`,而 Node 侧的 `fetch`(undici)不保证能解 ——
|
|
36
|
+
一旦解不开,SSE 会**静默变成乱码**(不是报错,是内容全错)。这是"宁可少一个头,也不要静默坏掉"的取舍,
|
|
37
|
+
代码里写了理由,用例里也钉住了(防止有人"顺手补上")。
|
|
38
|
+
|
|
39
|
+
⚠️ **一处保留的偏差**:真实浏览器**不发** `x-app-version`(7 个 `x-*` 里没有它),我们仍在发。
|
|
40
|
+
保留是为了不改变既有行为,代码里标了注释 —— 想更贴近浏览器,删掉那一行即可。
|
|
41
|
+
|
|
42
|
+
**新增 11 条用例**(`check-request-headers.mjs`,此前 `pickExtraHeaders` 只有 1 条、`buildDsHeaders` 零覆盖):
|
|
43
|
+
该收的收进来 / 不该收的别收(逐请求头 + `accept-encoding`)/ 顺序保留 / 抓来的真值赢过兜底 /
|
|
44
|
+
逐请求头必须用当前值(旧 token 旧 cookie 不许漏出去)。
|
|
45
|
+
四处变异确认变红:白名单改回 `x-`(3 条红)、兜底覆盖抓取值(1 条红)、关掉 `x-device-id` 判据(1 条红)、
|
|
46
|
+
以及重建后的一处产物断言。
|
|
47
|
+
|
|
48
|
+
> ⚠️ 写用例时踩了一次**虚断言**:第一版把"抓来的值"和兜底都写成 `2.5.0`,于是
|
|
49
|
+
> "兜底覆盖抓取值"这个变异体照样通过(两者无法区分)。改用 `88.8.8` 之后才真的能打红。
|
|
50
|
+
> 这正是"先自问这条用例在改之前是不是也绿"那条铁律的又一次实证。
|
|
51
|
+
|
|
5
52
|
## 0.6.29 — 2026-10-02
|
|
6
53
|
|
|
7
54
|
**修复:登录态过期(或账号被限流)之后,重新登录会"又新建一个网页端会话"、整段历史全量重发。**
|
package/lib/index.js
CHANGED
|
@@ -616,13 +616,21 @@ function accountsFootprint() {
|
|
|
616
616
|
* 2026-09-21 的现场:`acc_2df7cf2f` 正是 cookie 与 extraHeaders 双空,
|
|
617
617
|
* 且它 `capturedAt` 刷新后 19 秒就撞了 `code 9 / invalid ref file id`。
|
|
618
618
|
* ⚠️ 但这只是**时间相关**,不是已证实的因果 —— 所以这里只留痕,不据此阻断。
|
|
619
|
+
*
|
|
620
|
+
* 2026-10-02 补一条:**抓到了头、但里面没有 `x-device-id`** 也算缺陷。
|
|
621
|
+
* 依据:真实捕获(同日 03:42Z)里 `x-device-id` 是一个 UUID(数美设备指纹),
|
|
622
|
+
* 且上游对"缺少浏览器设备指纹"会直接返回 `biz_code=11 / RISK_DEVICE_DETECTED`。
|
|
623
|
+
* 所以缺了它就不是"信息少一点",而是**会被风控判定**。
|
|
624
|
+
* ⚠️ 只覆盖"有头但缺这一项";`extraHeaders` 整个为空的手工粘 token 路径**故意不碰**
|
|
625
|
+
* (那条路本来就没有任何浏览器头,是既有设计,另议)。
|
|
619
626
|
*/
|
|
620
627
|
function captureDefect(auth) {
|
|
621
628
|
if (!String(auth.token ?? "").trim()) return void 0;
|
|
622
629
|
const hasCookie = !!String(auth.cookie ?? "").trim();
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
return "本次捕获只拿到 token(cookie 与请求头都为空)—— 若之后出现图片引用被拒(code 9)之类的异常,优先怀疑这份凭证";
|
|
630
|
+
const headers = auth.extraHeaders ?? {};
|
|
631
|
+
const hasHeaders = Object.keys(headers).length > 0;
|
|
632
|
+
if (!hasCookie && !hasHeaders) return "本次捕获只拿到 token(cookie 与请求头都为空)—— 若之后出现图片引用被拒(code 9)之类的异常,优先怀疑这份凭证";
|
|
633
|
+
if (hasHeaders && !String(headers["x-device-id"] ?? "").trim()) return "本次捕获的请求头里没有 x-device-id(数美设备指纹)—— 上游对\"缺少浏览器设备指纹\"会直接判 RISK_DEVICE_DETECTED(biz_code=11),建议重新登录一次把这份凭证补全";
|
|
626
634
|
}
|
|
627
635
|
/** 当前生效的登录凭证(没有选择账号 → undefined)。 */
|
|
628
636
|
function readAuth() {
|
|
@@ -3642,29 +3650,40 @@ const DEFAULT_WASM_URL = "https://fe-static.deepseek.com/chat/static/sha3_wasm_b
|
|
|
3642
3650
|
/** 浏览器 UA 兜底(捕获失败时使用)。 */
|
|
3643
3651
|
const FALLBACK_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
3644
3652
|
/**
|
|
3653
|
+
* 网页端 `x-client-version` 的兜底值(**抓取失败时**才用)。
|
|
3654
|
+
*
|
|
3655
|
+
* 🔴 这个值会随网页端发版过时,而且**没有任何自动机制会发现它过时** ——
|
|
3656
|
+
* 旧实现写死 `2.0.0`,到 2026-10-02 已经落后到 `2.5.0`(真实捕获值)。
|
|
3657
|
+
* 所以:① 优先永远用抓来的真值(`auth.extraHeaders`);
|
|
3658
|
+
* ② 这里只保证"万一没有真值时,不至于差得离谱";③ 每次核对捕获时顺手校准一次。
|
|
3659
|
+
*/
|
|
3660
|
+
const FALLBACK_CLIENT_VERSION = "2.5.0";
|
|
3661
|
+
/**
|
|
3645
3662
|
* 组装一次网页端请求的头。
|
|
3646
|
-
*
|
|
3647
|
-
*
|
|
3648
|
-
*
|
|
3663
|
+
*
|
|
3664
|
+
* 顺序与优先级(2026-10-02 重排,理由见下):
|
|
3665
|
+
* ① 先铺**登录时从真实浏览器抓来的指纹头**(`auth.extraHeaders`)——
|
|
3666
|
+
* 头的**顺序本身就是指纹**,浏览器给什么顺序就用什么顺序;
|
|
3667
|
+
* 而且它的值比我们写死的兜底更可信(`x-client-version` 随网页端版本走,写死必然过时)。
|
|
3668
|
+
* ② 再补**兜底**,且只补浏览器没给的(写死值绝不能盖掉抓来的真值)。
|
|
3669
|
+
* ③ 最后放**逐请求现算**的头(token / cookie / hif / pow / content-type / origin / referer)——
|
|
3670
|
+
* 这些必须用**当前**登录态,复用快照里的旧值会出错。
|
|
3649
3671
|
*/
|
|
3650
3672
|
function buildDsHeaders(auth, referer) {
|
|
3651
|
-
const headers = {
|
|
3652
|
-
|
|
3673
|
+
const headers = { ...auth.extraHeaders ?? {} };
|
|
3674
|
+
const defaults = {
|
|
3653
3675
|
accept: "application/json, text/plain, */*",
|
|
3654
3676
|
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
3655
|
-
"content-type": "application/json",
|
|
3656
|
-
origin: DS_BASE,
|
|
3657
|
-
referer: referer || `https://chat.deepseek.com/`,
|
|
3658
3677
|
"x-client-platform": "web",
|
|
3659
|
-
"x-client-version":
|
|
3660
|
-
"x-app-version":
|
|
3661
|
-
...auth.extraHeaders ?? {}
|
|
3678
|
+
"x-client-version": FALLBACK_CLIENT_VERSION,
|
|
3679
|
+
"x-app-version": FALLBACK_CLIENT_VERSION
|
|
3662
3680
|
};
|
|
3663
|
-
|
|
3681
|
+
for (const [key, value] of Object.entries(defaults)) if (!headers[key]) headers[key] = value;
|
|
3682
|
+
headers["user-agent"] = auth.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
3664
3683
|
headers["content-type"] = "application/json";
|
|
3665
3684
|
headers.origin = DS_BASE;
|
|
3666
3685
|
headers.referer = referer || `https://chat.deepseek.com/`;
|
|
3667
|
-
headers
|
|
3686
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
3668
3687
|
headers["x-deepseek-harness"] = "deepseek-harness (+https://github.com/deepseek-ai/deepseek-harness); provider=deepseek-web";
|
|
3669
3688
|
delete headers["x-ds-pow-response"];
|
|
3670
3689
|
if (auth.cookie) headers.cookie = auth.cookie;
|
|
@@ -7499,19 +7518,62 @@ function buildCookieHeader(cookies) {
|
|
|
7499
7518
|
return (cookies ?? []).filter((cookie) => cookie && typeof cookie.name === "string" && String(cookie.domain ?? "").includes("deepseek")).map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
7500
7519
|
}
|
|
7501
7520
|
/**
|
|
7502
|
-
*
|
|
7503
|
-
*
|
|
7521
|
+
* 浏览器**指纹头**的名字判据 —— 只有"浏览器行为的一部分、且可跨请求复用"的才收。
|
|
7522
|
+
*
|
|
7523
|
+
* 这个判据**只能有一处**:原先它在 `browser-login.ts`(CDP 路径)和 `login.ts`
|
|
7524
|
+
* (旧的 webRequest 路径)各写了一遍,收的还只有 `x-*`。两份一旦漂移,
|
|
7525
|
+
* "同一账号用不同路径登录、指纹就不一样",而且不会有任何报错。
|
|
7526
|
+
*/
|
|
7527
|
+
const FINGERPRINT_HEADER_RE = /^(?:x-|sec-ch-ua|sec-fetch-)/;
|
|
7528
|
+
const FINGERPRINT_HEADER_EXACT = /* @__PURE__ */ new Set([
|
|
7529
|
+
"priority",
|
|
7530
|
+
"accept",
|
|
7531
|
+
"accept-language"
|
|
7532
|
+
]);
|
|
7533
|
+
/**
|
|
7534
|
+
* 逐请求**现算**的头 —— 收进快照就是 bug:下一轮用旧值会把当前登录态盖掉。
|
|
7535
|
+
*
|
|
7536
|
+
* ⚠️ `accept-encoding` 是**故意**收在外面的,理由不是"不该收"而是"收了会坏":
|
|
7537
|
+
* 它是浏览器的解压能力声明,服务端可能据此回 `zstd`,而 Node 侧的 `fetch`(undici)
|
|
7538
|
+
* 不保证能解 —— 一旦解不开,SSE 会**静默变成乱码**(不是报错,是内容全错)。
|
|
7539
|
+
* 宁可少一个头,也不要静默坏掉。将来若要收,必须先验证整条解压链。
|
|
7540
|
+
*/
|
|
7541
|
+
const PER_REQUEST_HEADERS = /* @__PURE__ */ new Set([
|
|
7542
|
+
"x-ds-pow-response",
|
|
7543
|
+
"x-hif-dliq",
|
|
7544
|
+
"x-hif-leim",
|
|
7545
|
+
"authorization",
|
|
7546
|
+
"cookie",
|
|
7547
|
+
"content-type",
|
|
7548
|
+
"content-length",
|
|
7549
|
+
"host",
|
|
7550
|
+
"connection",
|
|
7551
|
+
"transfer-encoding",
|
|
7552
|
+
"accept-encoding",
|
|
7553
|
+
"origin",
|
|
7554
|
+
"referer",
|
|
7555
|
+
"user-agent"
|
|
7556
|
+
]);
|
|
7557
|
+
/**
|
|
7558
|
+
* 从浏览器请求头里挑出可复用的**指纹头**(纯函数,便于单测)。
|
|
7559
|
+
*
|
|
7560
|
+
* 为什么重要(2026-10-02 核对真实捕获):
|
|
7561
|
+
* 旧规则只留 `x-*`,于是浏览器**自动加**的那一批全被丢掉 ——
|
|
7562
|
+
* `sec-ch-ua` / `sec-ch-ua-mobile` / `sec-ch-ua-platform`、`sec-fetch-dest/mode/site`、
|
|
7563
|
+
* `priority`。这些是"是不是真浏览器"最表层、最容易被看到的一批信号,
|
|
7564
|
+
* 而丢掉它们的代价是零成本就能避免的。
|
|
7565
|
+
*
|
|
7566
|
+
* ⚠️ 顺序必须保留:**头的顺序本身就是指纹**。`Object.entries` 按插入顺序遍历,
|
|
7567
|
+
* 而调用方是按浏览器给的顺序建这个对象的 ⇒ 收出来的 `out` 就是浏览器的相对顺序。
|
|
7504
7568
|
*/
|
|
7505
7569
|
function pickExtraHeaders(headers) {
|
|
7506
7570
|
const out = {};
|
|
7507
7571
|
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
7508
7572
|
const lower = key.toLowerCase();
|
|
7509
|
-
if (
|
|
7510
|
-
if (lower
|
|
7573
|
+
if (PER_REQUEST_HEADERS.has(lower)) continue;
|
|
7574
|
+
if (!FINGERPRINT_HEADER_RE.test(lower) && !FINGERPRINT_HEADER_EXACT.has(lower)) continue;
|
|
7511
7575
|
out[lower] = String(value);
|
|
7512
7576
|
}
|
|
7513
|
-
const acceptLanguage = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === "accept-language");
|
|
7514
|
-
if (acceptLanguage) out["accept-language"] = String(acceptLanguage[1]);
|
|
7515
7577
|
return out;
|
|
7516
7578
|
}
|
|
7517
7579
|
/** 极简 CDP 客户端:只需 send + 事件监听。 */
|
|
@@ -8577,16 +8639,7 @@ function hookHeaders(ses, buffer, onRewrite) {
|
|
|
8577
8639
|
if (cleanLower["cookie"]) buffer.cookie = cleanLower["cookie"];
|
|
8578
8640
|
if (cleanLower["x-hif-dliq"]) buffer.hifDliq = cleanLower["x-hif-dliq"];
|
|
8579
8641
|
if (cleanLower["x-hif-leim"]) buffer.hifLeim = cleanLower["x-hif-leim"];
|
|
8580
|
-
if (!buffer.extraHeaders["x-client-version"])
|
|
8581
|
-
const snapshot = {};
|
|
8582
|
-
for (const [key, value] of Object.entries(cleanLower)) {
|
|
8583
|
-
if (!/^x-/.test(key)) continue;
|
|
8584
|
-
if (key === "x-ds-pow-response" || key === "x-hif-dliq" || key === "x-hif-leim") continue;
|
|
8585
|
-
snapshot[key] = value;
|
|
8586
|
-
}
|
|
8587
|
-
if (cleanLower["accept-language"]) snapshot["accept-language"] = cleanLower["accept-language"];
|
|
8588
|
-
buffer.extraHeaders = snapshot;
|
|
8589
|
-
}
|
|
8642
|
+
if (!buffer.extraHeaders["x-client-version"]) buffer.extraHeaders = pickExtraHeaders(cleanLower);
|
|
8590
8643
|
}
|
|
8591
8644
|
callback({ requestHeaders: headers });
|
|
8592
8645
|
});
|
|
@@ -10259,7 +10312,7 @@ async function checkForUpdate(current, fetchImpl) {
|
|
|
10259
10312
|
* 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
|
|
10260
10313
|
*/
|
|
10261
10314
|
/** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
|
|
10262
|
-
const FALLBACK_VERSION = "0.6.
|
|
10315
|
+
const FALLBACK_VERSION = "0.6.30";
|
|
10263
10316
|
let cached;
|
|
10264
10317
|
/** 本插件版本(如 `0.1.26`)。 */
|
|
10265
10318
|
function pluginVersion() {
|