dsh-deepseek-web-login 0.6.5 → 0.6.7
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 +63 -0
- package/lib/client.js +90 -16
- package/lib/client.js.map +1 -1
- package/lib/index.js +170 -30
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/tools/panel-preview.mjs +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循大致语义化版本;日期为本地时间。
|
|
4
4
|
|
|
5
|
+
## 0.6.7 — 2026-09-30
|
|
6
|
+
|
|
7
|
+
**修「点切换像没反应、报错在列表底部看不见」+「探活超时被当成登录态失效」。**
|
|
8
|
+
|
|
9
|
+
现场(用户截图):点一个**正常**账号的「切换」,界面上像什么都没发生;过一会儿列表底部才冒出
|
|
10
|
+
一行红字 —— `切换失败:该账号登录态校验未通过(The operation was aborted due to timeout),
|
|
11
|
+
请重新登录后再切换`。用户的原话是"这样一点也不明显"。
|
|
12
|
+
|
|
13
|
+
两件事叠在一起:
|
|
14
|
+
|
|
15
|
+
- **失败分类又漏了一处**。0.6.6 给探活分了「授权类 / 网络类」,但**切换前那次探活没跟上**:
|
|
16
|
+
不管哪一类失败都拦下来、都建议"重新登录"。而那次真因只是超时(网络抖了一下),凭证完全可用。
|
|
17
|
+
- **反馈位置不对**。切换的反馈只写卡片底部那一条消息,而用户的视线还停在他点的那颗按钮上;
|
|
18
|
+
探活最长 20 秒,这段时间里按钮毫无变化 ⇒ 体感就是"点了没反应"。
|
|
19
|
+
|
|
20
|
+
改法:
|
|
21
|
+
|
|
22
|
+
1. **切换前探活按类型分流**(新纯函数 `switchGateFromProbe`,有专门用例守):
|
|
23
|
+
- 授权失效 ⇒ 拦下,说清「这个号的登录态已失效,需要重新登录一次才能切换」;
|
|
24
|
+
- 网络类(超时/断网) ⇒ **放行**,只提示「已切换,但没能校验(网络问题)—— 登录态未必失效」;
|
|
25
|
+
- 探活通过 ⇒ 直接切。
|
|
26
|
+
与 `staleAuthRecord` 同一取舍:**网络问题不该冒充授权结论**。
|
|
27
|
+
(判据本身还是纯函数,宿主真的会调用它 —— 用例里连这条也守着,防"判据写好了没人调用"。)
|
|
28
|
+
2. **交互路径的探活超时 20s → 10s**(`SWITCH_PROBE_TIMEOUT_MS`)。后台定时探活仍是 20s。
|
|
29
|
+
3. **反馈落在那一行**:新增行内提示条(`.dsw-rowmsg`),切换的结果直接写在账号行里;
|
|
30
|
+
按钮自身进入「切换中…」禁用态。列表重建后按账号 id 把结果写回新行(顺序反了会被冲掉)。
|
|
31
|
+
4. **「重登」同样落到行内** —— 它是同一类"慢操作 + 只在底部说话"的问题。
|
|
32
|
+
|
|
33
|
+
## 0.6.6 — 2026-09-29
|
|
34
|
+
|
|
35
|
+
**修「网络一抖,整库账号被标成需要重新登录,点重登还得重敲密码」。**
|
|
36
|
+
|
|
37
|
+
起因是用户申诉:面板上一排账号全写着「校验失败:fetch failed」,点「重登」之后浏览器
|
|
38
|
+
打开的是**登录页**,得重新填手机号 + 验证码。查下来是两件事叠在一起:
|
|
39
|
+
|
|
40
|
+
- **探活失败的两种性质没分开**:`fetch failed`(本次真因在 cause 里:
|
|
41
|
+
`net::ERR_NETWORK_IO_SUSPENDED`,机器休眠、网络挂起)与「token 失效」被写进**同一个字段**,
|
|
42
|
+
而这个字段的语义是"这个号必须重新登录" —— 徽章、账号轮换、重登是否清登录态全都读它。
|
|
43
|
+
一次休眠就把 9 个账号同时标红,凭证其实全是好的。
|
|
44
|
+
- **重登一进门就清登录态**:`!!lastVerifyError ⇒ 先清 profile + 登录分区`。
|
|
45
|
+
清除之后浏览器里什么都没有,于是必然要重新登录一遍 —— 这就是"点了还要我输密码"。
|
|
46
|
+
|
|
47
|
+
改法:
|
|
48
|
+
|
|
49
|
+
1. **写入点就分类**。只有授权类失败(401 / 403 / invalid token / 过期)写 `lastVerifyError`;
|
|
50
|
+
网络类(断网、超时、5xx、网络挂起)写新的 `lastCheckError`,且**不覆盖**已有的授权类结论
|
|
51
|
+
(否则真死号会被网络抖动洗白)。成功时两个标记一起清。
|
|
52
|
+
2. **重登先做一次只读探活**,按结果分三条路(判据是纯函数 `planRelogin`):
|
|
53
|
+
- 探活通过 ⇒ 账号本来就是好的:清掉标记,**不打开浏览器、不清任何东西**,一键结束;
|
|
54
|
+
- 网络类失败 ⇒ 只报网络问题,**不清登录态、不开窗口**(这时候重登必然白敲);
|
|
55
|
+
- 授权类失败 ⇒ 才清登录态 + 手动登录一次(原行为)。
|
|
56
|
+
3. **错误串保留原因链**:`fetch failed ← net::ERR_NETWORK_IO_SUSPENDED`。
|
|
57
|
+
以前只留最外层一句 `fetch failed`,既看不出是休眠还是 DNS,也没法判断该等网络还是该重登。
|
|
58
|
+
4. **面板**:徽章只在授权失效时显示「❌ 需要重新登录」,网络类显示中性的
|
|
59
|
+
「⚠️ 未能校验(网络)」;「校验全部」的汇总分开报"登录态已失效"与"网络没通、未能校验"。
|
|
60
|
+
5. **老数据自愈**:读账号时把历史上写错位置的网络类失败自动迁到 `lastCheckError`,
|
|
61
|
+
老库不必等下一次探活,也不会再被它误导。
|
|
62
|
+
|
|
63
|
+
用例:`check-relogin-integrity`(三条分支 + 不清登录态 + cause 保留)、
|
|
64
|
+
`check-account-add`(两个标记一起清、`carried` 白名单不漏新字段)、
|
|
65
|
+
`check-accounts`(读路径迁移)、`check-bundle`(产物里 `planRelogin` 三态**真求值** +
|
|
66
|
+
`describeError` 真的沿 cause 链)。反向验证 5/5 + 产物层 3/3 如期红。
|
|
67
|
+
|
|
5
68
|
## 0.6.5 — 2026-09-27
|
|
6
69
|
|
|
7
70
|
> 修复 0.6.3 的重试策略**形状**错误:它会让「按本地退避重试」算出 `NaN`,把整轮打成 UNKNOWN 错误。
|
package/lib/client.js
CHANGED
|
@@ -409,6 +409,17 @@ border:1px solid var(--bd);border-left:3px solid var(--fg3);white-space:pre-wrap
|
|
|
409
409
|
overflow:auto;font-size:12px;line-height:1.6;color:var(--fg)}
|
|
410
410
|
.dsw-msg.err{border-left-color:var(--err);color:color-mix(in srgb,var(--err) 78%,var(--fg))}
|
|
411
411
|
.dsw-msg.ok{border-left-color:var(--ok)}
|
|
412
|
+
.dsw-msg.warn{border-left-color:var(--warn);color:color-mix(in srgb,var(--warn) 80%,var(--fg))}
|
|
413
|
+
/* 行内结果条:由**某一行上的按钮**触发的操作(切换 / 重登),结果必须留在那一行。
|
|
414
|
+
为什么(2026-09-30 用户反馈「点切换没反应,报错在下面显示,一点也不明显」):
|
|
415
|
+
反馈原先只写卡片底部那条消息,用户点完按钮视线还在原处 —— 列表一长,等于没反馈。 */
|
|
416
|
+
.dsw-rowmsg{margin-top:6px;padding:6px 10px;border-radius:var(--r-sm);background:var(--bg2);
|
|
417
|
+
border-left:3px solid var(--fg3);color:var(--fg2);font-size:12px;line-height:1.55;
|
|
418
|
+
white-space:pre-wrap;word-break:break-word}
|
|
419
|
+
.dsw-rowmsg:empty{display:none}
|
|
420
|
+
.dsw-rowmsg.err{border-left-color:var(--err);color:color-mix(in srgb,var(--err) 78%,var(--fg))}
|
|
421
|
+
.dsw-rowmsg.ok{border-left-color:var(--ok)}
|
|
422
|
+
.dsw-rowmsg.warn{border-left-color:var(--warn);color:color-mix(in srgb,var(--warn) 80%,var(--fg))}
|
|
412
423
|
.dsw-models{list-style:none;margin:0;padding:0}
|
|
413
424
|
.dsw-models li{padding:10px 0;border-top:1px solid var(--bd)}
|
|
414
425
|
.dsw-models li:first-child{border-top:none;padding-top:2px}
|
|
@@ -900,30 +911,48 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
900
911
|
*/
|
|
901
912
|
const reloginAccount = (id, title) => {
|
|
902
913
|
(async () => {
|
|
903
|
-
accountsMsg.textContent =
|
|
914
|
+
accountsMsg.textContent = `正在校验「${title}」并准备登录窗口……`;
|
|
915
|
+
paintRowMsg(id, "正在校验登录态并准备登录窗口……", "info");
|
|
904
916
|
try {
|
|
905
917
|
const prep = await api("/login/relogin", {
|
|
906
918
|
method: "POST",
|
|
907
919
|
body: JSON.stringify({ id })
|
|
908
920
|
});
|
|
921
|
+
if (prep?.alreadyValid) {
|
|
922
|
+
const text = String(prep.hint ?? "这条账号校验通过,不需要重新登录");
|
|
923
|
+
accountsMsg.textContent = text;
|
|
924
|
+
paintRowMsg(id, `✅ ${text}`, "ok");
|
|
925
|
+
await loadAccounts();
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
909
928
|
if (prep?.ok === false) {
|
|
910
|
-
|
|
929
|
+
const text = String(prep?.error ?? "未知原因");
|
|
930
|
+
accountsMsg.textContent = text;
|
|
931
|
+
paintRowMsg(id, `⚠️ ${text}`, "warn");
|
|
911
932
|
return;
|
|
912
933
|
}
|
|
913
934
|
boostUntil = Date.now() + 3e5;
|
|
914
|
-
|
|
935
|
+
const opened = String(prep?.hint ?? "登录窗口已打开:如果浏览器里还留着这个账号的登录态会立刻复用,否则在里面重新登录一次……");
|
|
936
|
+
accountsMsg.textContent = opened;
|
|
937
|
+
paintRowMsg(id, opened, "info");
|
|
915
938
|
const result = await api("/login/browser", {
|
|
916
939
|
method: "POST",
|
|
917
940
|
body: "{}"
|
|
918
941
|
});
|
|
919
942
|
if (result?.started === false) {
|
|
920
|
-
|
|
943
|
+
const text = `打开登录窗口失败:${result?.reason ?? "未知原因"}(可改用「手动粘贴 Token」)`;
|
|
944
|
+
accountsMsg.textContent = text;
|
|
945
|
+
paintRowMsg(id, `❌ ${text}`, "err");
|
|
921
946
|
return;
|
|
922
947
|
}
|
|
923
|
-
|
|
948
|
+
const done = result?.relogin ? `「${title}」的凭证已原地更新(同一条记录、当前账号未变),旧的失败标记也清掉了。` : result?.added ? `「${title}」已加入账号库 —— 当前使用的账号没有改变。` : "已捕获并保存凭证。";
|
|
949
|
+
accountsMsg.textContent = done;
|
|
924
950
|
await loadAccounts();
|
|
951
|
+
paintRowMsg(id, `✅ ${done}`, "ok");
|
|
925
952
|
} catch (error) {
|
|
926
|
-
|
|
953
|
+
const message = `重新登录失败:${error?.message ?? error}`;
|
|
954
|
+
accountsMsg.textContent = message;
|
|
955
|
+
paintRowMsg(id, `❌ ${message}`, "err");
|
|
927
956
|
} finally {
|
|
928
957
|
await refresh(false).catch(() => void 0);
|
|
929
958
|
}
|
|
@@ -938,6 +967,7 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
938
967
|
if (item.unverified) title.append(el("span", "dsw-badge off", "❔ 未校验"));
|
|
939
968
|
if (item.limit && Number.isFinite(item.limit.untilMs) && item.limit.untilMs > Date.now()) title.append(el("span", "dsw-badge off", `⏳ 受限至 ${shortTime(item.limit.untilMs)}`));
|
|
940
969
|
if (item.lastVerifyError) title.append(el("span", "dsw-badge err", "❌ 需要重新登录"));
|
|
970
|
+
else if (item.lastCheckError) title.append(el("span", "dsw-badge off", "⚠️ 未能校验(网络)"));
|
|
941
971
|
main.append(title);
|
|
942
972
|
const meta = [];
|
|
943
973
|
if (item.display) meta.push(item.display);
|
|
@@ -946,19 +976,23 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
946
976
|
const cookieMeta = Array.isArray(item.cookieMeta) ? item.cookieMeta : [];
|
|
947
977
|
meta.push(`cookie:${describeCookieLife(summarizeCookieLife(cookieMeta))}`);
|
|
948
978
|
main.append(el("div", "dsw-account-meta", meta.join(" · ")));
|
|
949
|
-
if (item.lastVerifyError) main.append(el("div", "dsw-account-fix", `⚠️ ${relTime(item.lastVerifyError.at)}
|
|
979
|
+
if (item.lastVerifyError) main.append(el("div", "dsw-account-fix", `⚠️ ${relTime(item.lastVerifyError.at)}校验失败(登录态已失效):${item.lastVerifyError.message}`));
|
|
980
|
+
else if (item.lastCheckError) main.append(el("div", "dsw-account-fix", `⚠️ ${relTime(item.lastCheckError.at)}未能校验(网络问题,账号未必失效):${item.lastCheckError.message}`));
|
|
981
|
+
const rowMsg = el("div", "dsw-rowmsg");
|
|
982
|
+
rowMsg.dataset.rowMsg = String(item.id);
|
|
983
|
+
main.append(rowMsg);
|
|
950
984
|
row.append(main);
|
|
951
985
|
const actions = el("div", "dsw-account-actions");
|
|
952
|
-
if (item.lastVerifyError) {
|
|
986
|
+
if (item.lastVerifyError || item.lastCheckError) {
|
|
953
987
|
const reloginBtn = el("button", "dsw-btn dsw-preset", "重登");
|
|
954
|
-
reloginBtn.title = "
|
|
988
|
+
reloginBtn.title = "点一下会先做一次只读校验:凭证还能用就立刻恢复(不打开浏览器、不用敲密码);只有确认授权失效(401/过期)时才会清掉登录态、让你重新登录一次。捕获后原地更新这条记录,不新增、也不切换当前账号";
|
|
955
989
|
reloginBtn.addEventListener("click", () => reloginAccount(item.id, item.title || item.id));
|
|
956
990
|
actions.append(reloginBtn);
|
|
957
991
|
}
|
|
958
992
|
if (item.isActive) actions.append(el("span", "dsw-hint", "使用中"));
|
|
959
993
|
else {
|
|
960
994
|
const useBtn = el("button", "dsw-btn ghost dsw-preset", "切换");
|
|
961
|
-
useBtn.addEventListener("click", () => void switchToAccount(item.id));
|
|
995
|
+
useBtn.addEventListener("click", () => void switchToAccount(item.id, useBtn));
|
|
962
996
|
actions.append(useBtn);
|
|
963
997
|
}
|
|
964
998
|
const renameBtn = el("button", "dsw-btn ghost dsw-preset", "备注");
|
|
@@ -1143,7 +1177,13 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
1143
1177
|
return;
|
|
1144
1178
|
}
|
|
1145
1179
|
const failed = Number(result?.failed ?? 0);
|
|
1146
|
-
|
|
1180
|
+
const authFailed = Number(result?.authFailed ?? 0);
|
|
1181
|
+
const transportFailed = Number(result?.transportFailed ?? 0);
|
|
1182
|
+
const summary = [`${Number(result?.passed ?? 0)} 个正常`];
|
|
1183
|
+
if (authFailed > 0) summary.push(`${authFailed} 个登录态已失效(需要重登)`);
|
|
1184
|
+
if (transportFailed > 0) summary.push(`${transportFailed} 个网络没通、未能校验(账号未必失效)`);
|
|
1185
|
+
if (failed === 0) summary.length = 0;
|
|
1186
|
+
accountsMsg.textContent = failed > 0 ? `已校验 ${result?.checked ?? 0} 个账号:${summary.join("、")}。` : `已校验 ${result?.checked ?? 0} 个账号:全部正常。`;
|
|
1147
1187
|
} catch (error) {
|
|
1148
1188
|
accountsMsg.textContent = `刷新失败:${error?.message ?? error}`;
|
|
1149
1189
|
} finally {
|
|
@@ -1154,7 +1194,26 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
1154
1194
|
};
|
|
1155
1195
|
refreshAccountsBtn.addEventListener("click", () => void refreshAccountStates());
|
|
1156
1196
|
newGroupBtn.addEventListener("click", () => promptNewGroup());
|
|
1157
|
-
|
|
1197
|
+
/**
|
|
1198
|
+
* 把结果写进**某一行的行内提示条**(按账号 id 找回节点 —— 列表重建过也能写回新的那条)。
|
|
1199
|
+
*
|
|
1200
|
+
* 为什么要有它(2026-09-30 用户反馈「点切换没反应,报错在下面显示,一点也不明显」):
|
|
1201
|
+
* 切换的反馈原先只写卡片底部那一条,而用户的视线还停在他点的那颗按钮上 ——
|
|
1202
|
+
* 列表一长,那条消息在屏幕外,体感就是"点了没反应"。所以结果必须在**那一行**里出现。
|
|
1203
|
+
*/
|
|
1204
|
+
const paintRowMsg = (id, text, kind) => {
|
|
1205
|
+
const node = accountsList.querySelector(`.dsw-rowmsg[data-row-msg="${id}"]`);
|
|
1206
|
+
if (!node) return;
|
|
1207
|
+
node.className = kind === "info" ? "dsw-rowmsg" : `dsw-rowmsg ${kind}`;
|
|
1208
|
+
node.textContent = text;
|
|
1209
|
+
};
|
|
1210
|
+
const switchToAccount = async (id, btn) => {
|
|
1211
|
+
const originalLabel = btn?.textContent ?? "";
|
|
1212
|
+
if (btn) {
|
|
1213
|
+
btn.disabled = true;
|
|
1214
|
+
btn.textContent = "切换中…";
|
|
1215
|
+
}
|
|
1216
|
+
paintRowMsg(id, "正在校验登录态并切换……", "info");
|
|
1158
1217
|
accountsMsg.textContent = "切换中……";
|
|
1159
1218
|
try {
|
|
1160
1219
|
const result = await api("/accounts/switch", {
|
|
@@ -1162,15 +1221,30 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
1162
1221
|
body: JSON.stringify({ id })
|
|
1163
1222
|
});
|
|
1164
1223
|
if (!result?.ok) {
|
|
1165
|
-
|
|
1224
|
+
const message = `切换失败:${result?.error ?? "未知原因"}`;
|
|
1225
|
+
paintRowMsg(id, `❌ ${message}`, "err");
|
|
1226
|
+
accountsMsg.textContent = message;
|
|
1166
1227
|
return;
|
|
1167
1228
|
}
|
|
1168
|
-
accountsMsg.textContent = "已切换(下一次请求生效)。正在刷新登录状态……";
|
|
1169
1229
|
await loadAccounts();
|
|
1170
1230
|
await refresh(false);
|
|
1171
|
-
|
|
1231
|
+
if (result?.warning) {
|
|
1232
|
+
const text = `⚠️ ${String(result.warning)}`;
|
|
1233
|
+
paintRowMsg(id, text, "warn");
|
|
1234
|
+
accountsMsg.textContent = text;
|
|
1235
|
+
} else {
|
|
1236
|
+
paintRowMsg(id, "✅ 已切换(下一次请求生效)", "ok");
|
|
1237
|
+
accountsMsg.textContent = "已切换(下一次请求生效)";
|
|
1238
|
+
}
|
|
1172
1239
|
} catch (error) {
|
|
1173
|
-
|
|
1240
|
+
const message = `切换失败:${error?.message ?? error}`;
|
|
1241
|
+
paintRowMsg(id, `❌ ${message}`, "err");
|
|
1242
|
+
accountsMsg.textContent = message;
|
|
1243
|
+
} finally {
|
|
1244
|
+
if (btn) {
|
|
1245
|
+
btn.disabled = false;
|
|
1246
|
+
btn.textContent = originalLabel;
|
|
1247
|
+
}
|
|
1174
1248
|
}
|
|
1175
1249
|
};
|
|
1176
1250
|
const removeAccountById = async (id) => {
|