dsh-deepseek-web-login 0.2.2 → 0.3.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/lib/index.js CHANGED
@@ -874,6 +874,46 @@ function clampMaxRefImages(value) {
874
874
  if (!Number.isFinite(value)) return 24;
875
875
  return Math.max(MAX_REF_IMAGES_BOUNDS.min, Math.min(MAX_REF_IMAGES_BOUNDS.max, Math.round(value)));
876
876
  }
877
+ /**
878
+ * 对外声明的模型上下文窗口(token),即模型信息里的 `context.contextWindow`。
879
+ *
880
+ * 它决定的是**DSH 什么时候开始压缩/截断历史**,而不是插件发多少 —— 声明 1M,DSH 就认为
881
+ * "还装得下",于是一直不压,每轮都把整段转写重发。对只跑短任务的人来说,那部分体量是白烧的。
882
+ *
883
+ * ⚠️ 它和 `maxPromptChars` 是**两道独立阀门**,别混:
884
+ * - `maxPromptChars` 管"单次请求最多发多少字符"(插件自己切,见上);
885
+ * - `contextWindow` 管"DSH 认为模型能装多少"(DSH 据此决定压不压历史)。
886
+ * 两个都调小,单次体量才会真的降下来;只调一个,另一个仍会把请求撑大。
887
+ *
888
+ * 上限取 DeepSeek 标称的 1Mi(服务端自己的数字也都是 1024 的整数倍),也就是本插件
889
+ * 一直以来的硬编码值;下限 32K —— 再小的话工具目录(约 5.6 万字符)就先装不下了。
890
+ */
891
+ const CONTEXT_WINDOW_BOUNDS = {
892
+ min: 32768,
893
+ max: 1048576
894
+ };
895
+ /** 默认=原来的硬编码值(1Mi)—— 不动这个开关的人行为完全不变。 */
896
+ const DEFAULT_CONTEXT_WINDOW = 1048576;
897
+ /**
898
+ * 面板滑块的档位。
899
+ *
900
+ * **用档位而不是线性连续值**:32K→1M 是 32 倍跨度,线性拖动时前四分之三的行程都挤在低档位,
901
+ * 手感很差;档位化之后每一格都是"翻倍"这个直觉,也正好落在 1024 的整数倍上。
902
+ * 直接调接口传任意值仍被接受(只做边界夹取,不强制吸附)—— 档位只约束面板。
903
+ */
904
+ const CONTEXT_WINDOW_OPTIONS = [
905
+ 32768,
906
+ 65536,
907
+ 131072,
908
+ 262144,
909
+ 524288,
910
+ 1048576
911
+ ];
912
+ /** 规整上下文窗口:非数 → 默认;越界 → 夹到边界。 */
913
+ function clampContextWindow(value) {
914
+ if (!Number.isFinite(value)) return DEFAULT_CONTEXT_WINDOW;
915
+ return Math.max(CONTEXT_WINDOW_BOUNDS.min, Math.min(CONTEXT_WINDOW_BOUNDS.max, Math.round(value)));
916
+ }
877
917
  /** 距上次请求超过这么久就算"歇过了",连续计数归零。 */
878
918
  const LONG_RUN_IDLE_RESET_MS = 12e4;
879
919
  /** 间隔可选的推荐档位(设置页的快捷按钮用):[下限, 上限]。 */
@@ -965,6 +1005,7 @@ function readGateSettings() {
965
1005
  if (lrb) out.longRunBreakMs = lrb;
966
1006
  if (Number.isFinite(parsed?.maxPromptChars)) out.maxPromptChars = clampMaxPromptChars(Number(parsed.maxPromptChars));
967
1007
  if (Number.isFinite(parsed?.maxRefImages)) out.maxRefImages = clampMaxRefImages(Number(parsed.maxRefImages));
1008
+ if (Number.isFinite(parsed?.contextWindow)) out.contextWindow = clampContextWindow(Number(parsed.contextWindow));
968
1009
  return Object.keys(out).length > 0 ? out : void 0;
969
1010
  } catch {
970
1011
  return;
@@ -1116,6 +1157,7 @@ function createRequestGate(options = {}) {
1116
1157
  /** prompt 字符上限(同 cleanupMode:只是存着,执行在 adapter)。 */
1117
1158
  let maxPromptChars = clampMaxPromptChars(options.maxPromptChars ?? 4e5);
1118
1159
  let maxRefImages = clampMaxRefImages(options.maxRefImages ?? 24);
1160
+ let contextWindow = clampContextWindow(options.contextWindow ?? 1048576);
1119
1161
  let cleanupMode = options.sessionCleanup;
1120
1162
  let cleanupBatch = normalizeCleanupRange(options.cleanupBatch, CLEANUP_BATCH_BOUNDS);
1121
1163
  let cleanupDelayMs = normalizeCleanupRange(options.cleanupDelayMs, CLEANUP_DELAY_BOUNDS_MS);
@@ -1132,7 +1174,8 @@ function createRequestGate(options = {}) {
1132
1174
  longRunThreshold,
1133
1175
  ...longRunBreakMs ? { longRunBreakMs } : {},
1134
1176
  maxPromptChars,
1135
- maxRefImages
1177
+ maxRefImages,
1178
+ contextWindow
1136
1179
  };
1137
1180
  }
1138
1181
  function configure(next) {
@@ -1142,6 +1185,7 @@ function createRequestGate(options = {}) {
1142
1185
  if (next.sessionCleanup !== void 0) cleanupMode = next.sessionCleanup;
1143
1186
  if (next.maxPromptChars !== void 0) maxPromptChars = clampMaxPromptChars(Number(next.maxPromptChars));
1144
1187
  if (next.maxRefImages !== void 0) maxRefImages = clampMaxRefImages(Number(next.maxRefImages));
1188
+ if (next.contextWindow !== void 0) contextWindow = clampContextWindow(Number(next.contextWindow));
1145
1189
  if (next.cleanupBatch !== void 0) {
1146
1190
  const value = normalizeCleanupRange(next.cleanupBatch, CLEANUP_BATCH_BOUNDS);
1147
1191
  if (value) cleanupBatch = value;
@@ -1395,10 +1439,16 @@ function muteUntilMs(json) {
1395
1439
  if (!Number.isFinite(seconds) || seconds <= 0) return void 0;
1396
1440
  return Math.round(seconds * 1e3);
1397
1441
  }
1398
- /** 被限制时的用户可读文案(带解除时间)。 */
1442
+ /**
1443
+ * 被限制时的用户可读文案。
1444
+ *
1445
+ * 只说结论与解除时间 —— **不解释原因**。原因(免费网页端对高频自动化的静默限流、
1446
+ * 登录态其实有效、只有 completion 被拒……)属于开发/排查信息,写在这里就够了:
1447
+ * 用户看到报错时只需要知道「是什么 + 到几点结束」,长篇解释只会淹没这两件事。
1448
+ */
1399
1449
  function mutedMessage(untilMs) {
1400
- if (untilMs === void 0) return "DeepSeek 网页端已临时限制本账号(user is muted),未给出解除时间。这期间任何网页模型调用都会失败;请等待解除,或改用官方 API key。";
1401
- return `DeepSeek 网页端已临时限制本账号(user is muted):预计 ${new Date(untilMs).toLocaleString("zh-CN", { hour12: false })} 解除,约 ${Math.max(1, Math.round((untilMs - Date.now()) / 6e4))} 分钟后。这期间任何网页模型调用都会失败(登录态本身有效、建会话也正常,只有发消息被拒);请等待解除,或改用官方 API key。免费网页端对高频自动化调用会静默限流,刚跑过大量工具步骤的会话尤其容易被限。`;
1450
+ if (untilMs === void 0) return "DeepSeek 网页端已封禁本账号(未给出解除时间)";
1451
+ return `DeepSeek 网页端已封禁本账号,${new Date(untilMs).toLocaleString("zh-CN", { hour12: false })} 解除(约 ${Math.max(1, Math.round((untilMs - Date.now()) / 6e4))} 分钟)`;
1402
1452
  }
1403
1453
  /**
1404
1454
  * 「同一账号同时只能生成一条」的并发拒绝(实测 2026-09-11:两个 DSH 窗口共用同一网页账号,
@@ -2867,7 +2917,7 @@ async function openCompletion(auth, params, signal, transport) {
2867
2917
  const text = await resp.text().catch(() => "");
2868
2918
  const code = httpErrorCode(resp.status);
2869
2919
  const retryAfter = parseRetryAfterMs(resp.headers.get("retry-after"));
2870
- const hint = code === "AUTH" ? " —— 网页登录态可能已过期,请到「设置 → DeepSeek 网页登录」重新登录" : code === "RATE_LIMIT" ? " —— 网页端频控(免费额度),稍后重试即可" : "";
2920
+ const hint = code === "AUTH" ? " —— 网页登录态可能已过期,请到「设置 → DeepSeek 网页登录」重新登录" : code === "RATE_LIMIT" ? " —— 网页版限流" : "";
2871
2921
  retireSession(sessionId);
2872
2922
  params.onDeleteSession?.(sessionId);
2873
2923
  throw new AdapterLlmError(`DeepSeek web completion failed (HTTP ${resp.status})${text ? `: ${text.slice(0, 200)}` : ""}${hint}`, code, {
@@ -2899,7 +2949,7 @@ async function openCompletion(auth, params, signal, transport) {
2899
2949
  const muted = isMutedError(biz);
2900
2950
  const busy = !muted && !!biz && isBusyGenerating(biz.msg);
2901
2951
  const untilMs = muteUntilMs(parsed);
2902
- const failure = biz ? new AdapterLlmError(muted ? mutedMessage(untilMs) : busy ? "DeepSeek 网页端同一账号同时只能生成一条消息(另一个窗口/标签页正在用同一账号生成)。这一步会自动重试;若两个窗口都要用网页模型,建议其中一个换 provider 或换账号。" : bizErrorMessage(biz.code, biz.msg), muted || busy ? "RATE_LIMIT" : isInvalidSessionError(biz) ? "TRANSPORT" : isInvalidRefFileError(biz) ? "INVALID_REF_FILE" : bizErrorCode(biz.code), {
2952
+ const failure = biz ? new AdapterLlmError(muted ? mutedMessage(untilMs) : busy ? "网页版限流:同一账号同时只能生成一条消息,稍后自动重试" : bizErrorMessage(biz.code, biz.msg), muted || busy ? "RATE_LIMIT" : isInvalidSessionError(biz) ? "TRANSPORT" : isInvalidRefFileError(biz) ? "INVALID_REF_FILE" : bizErrorCode(biz.code), {
2903
2953
  status: resp.status,
2904
2954
  ...muted && untilMs !== void 0 ? { providerRetryAfterMs: Math.max(0, untilMs - Date.now()) } : {},
2905
2955
  ...muted && untilMs !== void 0 ? { mutedUntilMs: untilMs } : {},
@@ -5160,10 +5210,19 @@ function modelInfoFor(provider, spec, requestedId) {
5160
5210
  inputModalities: ["text", "image"]
5161
5211
  };
5162
5212
  }
5163
- function resolvedModelInfo(provider, spec, requestedId) {
5213
+ /**
5214
+ * 组装模型信息。
5215
+ *
5216
+ * `contextWindowOverride` 来自设置页的「上下文范围」滑块(经 gate.json → index.ts → AdapterConfig)。
5217
+ * 传 undefined 就沿用 MODEL_SPECS 里的标称值(1Mi)—— 也就是不调这个开关时的原行为。
5218
+ *
5219
+ * ⚠️ 这个函数**每次请求都会被调用**(见下方 resolveModel / stream),所以滑块改完是即时生效的,
5220
+ * 不需要重启 DSH。
5221
+ */
5222
+ function resolvedModelInfo(provider, spec, requestedId, contextWindowOverride) {
5164
5223
  return {
5165
5224
  ...modelInfoFor(provider, spec, requestedId),
5166
- context: { contextWindow: spec.contextWindow },
5225
+ context: { contextWindow: contextWindowOverride ?? spec.contextWindow },
5167
5226
  defaultMaxTokens: spec.maxOutputTokens,
5168
5227
  reasoning: spec.configurableThinking ? {
5169
5228
  efforts: REASONING_EFFORTS,
@@ -5334,7 +5393,7 @@ function createAdapter(deps) {
5334
5393
  return Promise.resolve(MODEL_SPECS.map((spec) => modelInfoFor(provider, spec)));
5335
5394
  },
5336
5395
  resolveModel(provider, model) {
5337
- return Promise.resolve(resolvedModelInfo(provider, resolveSpec(model), String(model ?? "")));
5396
+ return Promise.resolve(resolvedModelInfo(provider, resolveSpec(model), String(model ?? ""), deps.config.contextWindow));
5338
5397
  },
5339
5398
  /**
5340
5399
  * 运行时契约(dsh-llm 0.1.2-rc.1):dispatch 前先取「精确模型元数据 + 该次调用的 stream」。
@@ -5343,7 +5402,7 @@ function createAdapter(deps) {
5343
5402
  prepareCall(provider, model, _signal) {
5344
5403
  const spec = resolveSpec(model);
5345
5404
  return Promise.resolve({
5346
- model: resolvedModelInfo(provider, spec, String(model ?? "")),
5405
+ model: resolvedModelInfo(provider, spec, String(model ?? ""), deps.config.contextWindow),
5347
5406
  stream: (options) => gatedStream(options)
5348
5407
  });
5349
5408
  },
@@ -5792,7 +5851,7 @@ function createAdapter(deps) {
5792
5851
  if (isContextTooLong(event.message)) throw new AdapterLlmError(`DeepSeek 网页端上下文超限:${event.message}`, "CONTEXT_WINDOW_EXCEEDED");
5793
5852
  if (event.code === "RATE_LIMIT") {
5794
5853
  const throttled = event.rateLimitKind === "throttled";
5795
- throw new AdapterLlmError(throttled ? `DeepSeek 网页端对这个账号限流了(发得太频繁)。这不是封号:登录态有效、建会话也正常,只有发消息被拒。这一步会自动退避重试;若一直不过,请等几分钟再继续,或降低自动化步骤密度(每一轮工具调用都是一次网页端请求)。` : `DeepSeek 网页端同一账号同时只能生成一条消息(另一个窗口/标签页正在用同一账号生成)。这一步会自动重试;若两个窗口都要用网页模型,建议其中一个换 provider 或换账号。`, "RATE_LIMIT", {
5854
+ throw new AdapterLlmError(throttled ? "网页版限流:发得太频繁,稍后自动重试" : "网页版限流:同一账号同时只能生成一条消息,稍后自动重试", "RATE_LIMIT", {
5796
5855
  ...event.retryAfterMs !== void 0 ? { providerRetryAfterMs: event.retryAfterMs } : {},
5797
5856
  ...throttled ? { rateLimitKind: "throttled" } : {}
5798
5857
  });
@@ -8070,7 +8129,7 @@ async function checkForUpdate(current, fetchImpl) {
8070
8129
  * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
8071
8130
  */
8072
8131
  /** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
8073
- const FALLBACK_VERSION = "0.2.2";
8132
+ const FALLBACK_VERSION = "0.3.1";
8074
8133
  let cached;
8075
8134
  /** 本插件版本(如 `0.1.26`)。 */
8076
8135
  function pluginVersion() {
@@ -8601,6 +8660,7 @@ function apply(ctx, config = {}) {
8601
8660
  longRunThreshold: savedGate?.longRunThreshold ?? 15,
8602
8661
  maxPromptChars: savedGate?.maxPromptChars ?? config.maxPromptChars ?? 4e5,
8603
8662
  maxRefImages: savedGate?.maxRefImages ?? config.maxRefImages ?? 24,
8663
+ contextWindow: savedGate?.contextWindow ?? 1048576,
8604
8664
  longRunBreakMs: savedGate?.longRunBreakMs,
8605
8665
  sessionCleanup: cleanupMode,
8606
8666
  cleanupBatch: cleanupBatchRange,
@@ -8621,6 +8681,7 @@ function apply(ctx, config = {}) {
8621
8681
  const adapterConfig = {
8622
8682
  maxPromptChars: savedGate?.maxPromptChars ?? config.maxPromptChars ?? 4e5,
8623
8683
  maxRefImages: savedGate?.maxRefImages ?? config.maxRefImages ?? 24,
8684
+ contextWindow: savedGate?.contextWindow ?? 1048576,
8624
8685
  idleTimeoutMs: config.idleTimeoutMs ?? 12e4,
8625
8686
  deleteWebSessions: config.deleteWebSessions !== false,
8626
8687
  sessionReuseTurns: config.sessionReuseTurns ?? 20,
@@ -8790,6 +8851,9 @@ function apply(ctx, config = {}) {
8790
8851
  maxPromptCharsDefault: DEFAULT_MAX_PROMPT_CHARS,
8791
8852
  maxRefImagesBounds: MAX_REF_IMAGES_BOUNDS,
8792
8853
  maxRefImagesDefault: 24,
8854
+ contextWindowBounds: CONTEXT_WINDOW_BOUNDS,
8855
+ contextWindowDefault: DEFAULT_CONTEXT_WINDOW,
8856
+ contextWindowOptions: CONTEXT_WINDOW_OPTIONS,
8793
8857
  cleanup: sessionCleaner.policy(),
8794
8858
  cleanupBounds: {
8795
8859
  batch: CLEANUP_BATCH_BOUNDS,
@@ -8881,6 +8945,17 @@ function apply(ctx, config = {}) {
8881
8945
  }
8882
8946
  patch.maxRefImages = clampMaxRefImages(count);
8883
8947
  }
8948
+ if (body.contextWindow !== void 0) {
8949
+ const window = Number(body.contextWindow);
8950
+ if (!Number.isFinite(window)) {
8951
+ sendJson(res, 400, {
8952
+ ok: false,
8953
+ error: "contextWindow 必须是数字"
8954
+ });
8955
+ return;
8956
+ }
8957
+ patch.contextWindow = clampContextWindow(window);
8958
+ }
8884
8959
  if (Object.keys(patch).length === 0) {
8885
8960
  sendJson(res, 400, {
8886
8961
  ok: false,
@@ -8891,6 +8966,7 @@ function apply(ctx, config = {}) {
8891
8966
  const applied = gate.configure(patch);
8892
8967
  if (applied.maxPromptChars !== void 0) adapterConfig.maxPromptChars = applied.maxPromptChars;
8893
8968
  if (applied.maxRefImages !== void 0) adapterConfig.maxRefImages = applied.maxRefImages;
8969
+ if (applied.contextWindow !== void 0) adapterConfig.contextWindow = applied.contextWindow;
8894
8970
  if (patch.sessionCleanup) sessionCleaner.configure({ mode: patch.sessionCleanup });
8895
8971
  const rangePatch = {};
8896
8972
  if (patch.cleanupBatch) rangePatch.batchRange = patch.cleanupBatch;