dsh-deepseek-web-login 0.5.2 → 0.6.0

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,68 @@
2
2
 
3
3
  本项目遵循大致语义化版本;日期为本地时间。
4
4
 
5
+ ## 0.6.0 — 2026-09-27
6
+
7
+ > 工具目录改用**紧凑类型签名**下发,不再给模型贴原始 JSON。
8
+
9
+ ### 改进
10
+
11
+ - **每个工具的参数不再原样贴 JSON**。原先那行是
12
+ `Parameters (JSON Schema): {"type":"object","properties":{…}}`,现在渲染成类型签名:
13
+
14
+ ```
15
+ ### read_file
16
+ Read a file.
17
+ read_file(file_path: string, offset?: number, limit?: number)
18
+ file_path: Path to read, resolved by the filesystem backend.
19
+ offset: 1-based first line to return. Defaults to 1.
20
+ ```
21
+
22
+ 模型要写出 `arguments`,真正需要的只是**参数名、类型、必填性**;那串 JSON 里
23
+ `"type":"…"`、键名的引号、每个参数各套一层对象,全是结构性样板。
24
+ - **两种参数形态都认**。DSH 自家工具是**扁平**写法(顶层键直接是参数名、`required: true`
25
+ 挂在参数自己身上),而 `@deepseek-ai/dsh-tools` 的 `schemaOf()` 把它**原样透传**、不做规范化;
26
+ 标准 JSON Schema 包裹形态(`{type:'object',properties:{…}}`)一并支持。
27
+ - 嵌套对象/数组/枚举/`anyOf` 都能渲染:`{content: string, status: string}[]`、
28
+ `"view" | "create" | "str_replace" | "insert"`。
29
+
30
+ ### 实测(2026-09-27,取自 13 个真实 DSH 工具定义)
31
+
32
+ - 参数段 **7,588 → 4,186 字符(省 45%)**,`required` 与参数描述全部保留、零回退。
33
+ - 折算到**整个工具目录约省 18%** —— 目录里更大的一块是工具级描述(同批样本 10,369 字符),
34
+ 那块**没动**:它藏的是"遇错怎么办"的指引,不许砍。
35
+ - 按 61 个工具 / 50,942 字符的目录估,约 **省 9,500 字符 ≈ 2,400 token**,占单轮输入 **3%** 左右。
36
+ ⇒ 别指望它解决"提示词太长":真正的大头是消息历史与环境注入,不在工具目录。
37
+
38
+ ### 说明 / 风险
39
+
40
+ - ⚠️ **这是 head 的变化**:升级后**第一个任务会全量重发一次**(投喂链断),之后照常。
41
+ - ⚠️ 模型看到的参数形态变了(JSON → 类型签名)。签名是模型最熟的形态,预期更好读;
42
+ 但**首次实测若发现参数写错,请立刻反馈** —— 回退只改一处:`buildToolSection` 里不用
43
+ `buildToolSignature` 即退回原始 JSON(回退分支与"认不出就不许发空参数"由
44
+ `tests/check-tool-signature.mjs` 守着)。
45
+ - 参数描述超过 160 字符会被截断;**工具级描述仍是 3200 上限,不受影响**。
46
+
47
+ ## 0.5.3 — 2026-09-27
48
+
49
+ > 修掉一个"重启后前 3 分钟里限流不换号"的窗口 —— 0.5.2 的漏洞。
50
+
51
+ ### 修复
52
+
53
+ - **限流换号的冷却用错了基准**:0.5.2 里冷却要求「距上次换号 ≥ 3 分钟」,而"上次换号"
54
+ 实际上取的是**插件启动时刻**(它同时还要给"按时间轮换"当计时起点)。两件事的语义不同 ——
55
+ 一个是"从启动算起过了多久",一个是"上次真的换过号是什么时候" —— 在"启动后还没换过号"
56
+ 这段区间里,它们给出**相反**的答案。
57
+ 后果:**每次重启之后的 3 分钟内撞上限流,都不会换号**。而那恰好是最容易撞上的窗口
58
+ (重启往往就是为了接着跑任务)。
59
+ 现在冷却改用独立的"上次真的换过号"时刻(只在换号成功与手动切号时推进):
60
+ **本次启动还没换过号 ⇒ 不套冷却**,限流立刻可换。
61
+
62
+ ### 说明
63
+
64
+ - 只影响"限流换号"这条路径的时间判定;按时间轮换、封禁换号、退避分流都不变。
65
+ - ⚠️ **升级后需要重启 DSH 才生效**(`link:` 安装方式不热重载)。
66
+
5
67
  ## 0.5.2 — 2026-09-27
6
68
 
7
69
  > 限流时也能自动换号接下去,不用再手动重发。
package/lib/index.js CHANGED
@@ -3443,24 +3443,167 @@ function truncate(text, max) {
3443
3443
  if (text.length <= max) return text;
3444
3444
  return `${text.slice(0, max - 3)}...`;
3445
3445
  }
3446
- /** 渲染工具目录(含 JSON Schema)。 */
3446
+ /**
3447
+ * 参数级描述的截断长度。
3448
+ *
3449
+ * 紧凑签名保留的是「参数名 + 类型 + 必填性」—— 那是模型写出正确 arguments 的**最小信息集**。
3450
+ * 参数描述是锦上添花:第 160 字符之后通常是举例或边角说明(`path` 这类参数压根没有),
3451
+ * 砍掉它比砍掉工具级描述划算得多 —— 工具级描述里藏的是「遇错怎么办」。
3452
+ */
3453
+ const MAX_PARAM_DESC_CHARS = 160;
3454
+ /** 嵌套展开的深度上限:异常 schema(自引用、深层嵌套)不该把工具目录撑爆。 */
3455
+ const MAX_SIGNATURE_DEPTH = 4;
3456
+ /** `parameters` 是对象时才当映射用(`parameters: []` 这类异常值直接忽略)。 */
3457
+ function asRecord(value) {
3458
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3459
+ }
3460
+ /**
3461
+ * 「标准 JSON Schema 包裹」形态的顶层关键字。顶层**只**出现这些键 ⇒ 是
3462
+ * `{type:'object',properties:{…},required:[…]}` 包裹;出现别的键 ⇒ 那些键就是参数名。
3463
+ */
3464
+ const SCHEMA_TOP_KEYS = /* @__PURE__ */ new Set([
3465
+ "type",
3466
+ "properties",
3467
+ "required",
3468
+ "additionalProperties",
3469
+ "description",
3470
+ "title",
3471
+ "default",
3472
+ "examples",
3473
+ "$schema",
3474
+ "definitions",
3475
+ "$defs"
3476
+ ]);
3477
+ /**
3478
+ * `parameters` 是「标准包裹」还是「DSH 扁平写法」。
3479
+ *
3480
+ * ⚠️ 两种都得认(2026-09-27 读 `@deepseek-ai/dsh-tools/lib/index.js` 的 `schemaOf()` 确认):
3481
+ * 它把工具定义里的 `parameters` **原样透传**给 provider,不做任何规范化。而 DSH 自家工具
3482
+ * 清一色写**扁平**形态 —— 顶层键直接是参数名、`required: true` 写在参数自己身上:
3483
+ *
3484
+ * parameters: { command: { type: 'string', required: true, description: '…' },
3485
+ * description: { type: 'string', required: true, description: '…' } }
3486
+ *
3487
+ * 常见的标准包裹形态仍要支持(第三方工具可能按 OpenAI 惯例写)。
3488
+ */
3489
+ function isWrappedSchema(node) {
3490
+ const properties = asRecord(node.properties);
3491
+ if (!properties || Object.keys(properties).length === 0) return false;
3492
+ if (!Object.values(properties).every((value) => asRecord(value) !== null)) return false;
3493
+ return Object.keys(node).every((key) => SCHEMA_TOP_KEYS.has(key));
3494
+ }
3495
+ /** 参数自己身上的必填标记(DSH 扁平写法)。 */
3496
+ function isRequiredParam(def) {
3497
+ return asRecord(def)?.required === true;
3498
+ }
3499
+ /** 读参数描述并压平空白。 */
3500
+ function paramDescription(schema) {
3501
+ const description = asRecord(schema)?.description;
3502
+ return typeof description === "string" ? description.replace(/\s+/g, " ").trim() : "";
3503
+ }
3504
+ /**
3505
+ * 渲染**一层参数映射**(`{参数名: 定义}`)→ `['a: string', 'b?: number']`。
3506
+ *
3507
+ * ⚠️ `depth` 必须**由调用方透传**。第一版在这里硬编码了 `1`,于是每下沉一层深度就重置,
3508
+ * `MAX_SIGNATURE_DEPTH` 完全失效 —— 深层嵌套(甚至自引用 schema)会一路展开到栈溢出,
3509
+ * 再被 catch 吞掉,表现为"莫名其妙回退到原始 JSON"。
3510
+ */
3511
+ function renderParamMap(map, requiredList, depth) {
3512
+ const fromList = new Set(requiredList);
3513
+ return Object.entries(map).map(([name, def]) => {
3514
+ return `${name}${fromList.has(name) || isRequiredParam(def) ? "" : "?"}: ${renderParamType(def, depth)}`;
3515
+ });
3516
+ }
3517
+ /**
3518
+ * 渲染一个参数的类型表达式:`string` / `T[]` / `{a: T, b?: U}` / `"x" | "y"`。
3519
+ *
3520
+ * 认不出来的形态一律退化成 `any` —— 签名本身永远合法。只有整份 parameters
3521
+ * 都不敢降级时,才由 `buildToolSignature` 返回 null 让调用方回退到原始 JSON。
3522
+ */
3523
+ function renderParamType(schema, depth) {
3524
+ const node = asRecord(schema);
3525
+ if (!node) return "any";
3526
+ if (depth > MAX_SIGNATURE_DEPTH) return "any";
3527
+ if (Array.isArray(node.enum) && node.enum.length > 0) return node.enum.map((value) => JSON.stringify(value)).join(" | ");
3528
+ const variants = Array.isArray(node.anyOf) ? node.anyOf : Array.isArray(node.oneOf) ? node.oneOf : null;
3529
+ if (variants && variants.length > 0) {
3530
+ const rendered = variants.map((item) => renderParamType(item, depth + 1));
3531
+ return [...new Set(rendered)].join(" | ");
3532
+ }
3533
+ const declared = typeof node.type === "string" ? node.type : "";
3534
+ if (declared === "array" || !declared && node.items !== void 0) {
3535
+ const item = renderParamType(node.items, depth + 1);
3536
+ return item.includes("|") ? `(${item})[]` : `${item}[]`;
3537
+ }
3538
+ const properties = asRecord(node.properties);
3539
+ if (declared === "object" || properties) {
3540
+ const inner = properties ? renderParamMap(properties, [], depth + 1) : [];
3541
+ const extra = asRecord(node.additionalProperties);
3542
+ if (extra) inner.push(`[key: string]: ${renderParamType(extra, depth + 1)}`);
3543
+ if (inner.length === 0) return "object";
3544
+ return `{${inner.join(", ")}}`;
3545
+ }
3546
+ return declared || "any";
3547
+ }
3548
+ /**
3549
+ * 把工具参数渲染成紧凑签名(0.6.0):
3550
+ *
3551
+ * read_file(file_path: string, offset?: number, limit?: number)
3552
+ * file_path: Path to read, resolved by the filesystem backend.
3553
+ *
3554
+ * 为什么值得做:原先直接贴 `JSON.stringify(parameters)`。那串文本里**结构性样板**占了大头
3555
+ * —— 每个参数都要套一层 `{"type":"…","description":"…"}`、键名与类型值全带引号,
3556
+ * 而模型写出 arguments 真正需要的只是**参数名、类型、必填性**。
3557
+ *
3558
+ * ⚠️ 返回 `null` = "这份 parameters 不敢降级",调用方必须回退到原始 JSON:
3559
+ * 宁可多花字符,也不能让模型看不见参数。
3560
+ */
3561
+ function buildToolSignature(tool) {
3562
+ try {
3563
+ const node = asRecord(tool.parameters);
3564
+ if (!node) return `${tool.name}()`;
3565
+ const wrapped = isWrappedSchema(node);
3566
+ const map = wrapped ? asRecord(node.properties) : node;
3567
+ const args = renderParamMap(map, wrapped && Array.isArray(node.required) ? node.required.filter((key) => typeof key === "string") : [], 1);
3568
+ if (args.length === 0) return Object.keys(node).length === 0 ? `${tool.name}()` : null;
3569
+ const lines = [`${tool.name}(${args.join(", ")})`];
3570
+ for (const [name, def] of Object.entries(map)) {
3571
+ const description = paramDescription(def);
3572
+ if (description) lines.push(` ${name}: ${truncate(description, MAX_PARAM_DESC_CHARS)}`);
3573
+ }
3574
+ return lines.join("\n");
3575
+ } catch {
3576
+ return null;
3577
+ }
3578
+ }
3579
+ /**
3580
+ * 渲染工具目录。
3581
+ *
3582
+ * 每个工具占一段:`### 名字` / 工具描述(≤3200 字符,不轻易砍)/ **参数紧凑签名**。
3583
+ * 签名由 `buildToolSignature` 产出;它只在 schema 不敢降级时返回 null,那时才贴原始 JSON Schema。
3584
+ */
3447
3585
  function buildToolSection(tools, maxChars = MAX_TOOLS_SECTION_CHARS) {
3448
3586
  if (!tools || tools.length === 0) return "";
3449
3587
  const parts = ["", "## Available tools"];
3450
3588
  let budget = Math.max(0, Math.min(MAX_TOOLS_SECTION_CHARS, maxChars));
3451
3589
  for (let index = 0; index < tools.length; index += 1) {
3452
3590
  const tool = tools[index];
3453
- let schemaText = "";
3454
- try {
3455
- schemaText = JSON.stringify(tool.parameters ?? {});
3456
- } catch {
3457
- schemaText = "{}";
3591
+ const signature = buildToolSignature(tool);
3592
+ let parametersText = signature ?? "";
3593
+ if (signature === null) {
3594
+ let schemaText = "";
3595
+ try {
3596
+ schemaText = JSON.stringify(tool.parameters ?? {});
3597
+ } catch {
3598
+ schemaText = "{}";
3599
+ }
3600
+ parametersText = `Parameters (JSON Schema): ${schemaText}`;
3458
3601
  }
3459
3602
  const block = [
3460
3603
  "",
3461
3604
  `### ${tool.name}`,
3462
3605
  truncate(String(tool.description ?? "").replace(/\s+/g, " ").trim(), MAX_DESCRIPTION_CHARS),
3463
- `Parameters (JSON Schema): ${schemaText}`
3606
+ parametersText
3464
3607
  ].join("\n");
3465
3608
  if (budget - block.length < 0) {
3466
3609
  const rest = tools.slice(index).map((item) => String(item?.name ?? "")).filter(Boolean);
@@ -6363,7 +6506,7 @@ function decideAutoSwitch(params) {
6363
6506
  const currentUnusable = current !== void 0 && !isUsable(current, now);
6364
6507
  const throttleSwitch = isThrottleSwitchAllowed({
6365
6508
  throttledAt: currentId === void 0 ? void 0 : params.throttledAt?.get(currentId),
6366
- lastSwitchAt,
6509
+ lastSwitchAt: params.lastSwitchedAt ?? 0,
6367
6510
  now,
6368
6511
  ...params.throttleWindowMs === void 0 ? {} : { windowMs: params.throttleWindowMs },
6369
6512
  ...params.throttleCooldownMs === void 0 ? {} : { cooldownMs: params.throttleCooldownMs }
@@ -8648,7 +8791,7 @@ async function checkForUpdate(current, fetchImpl) {
8648
8791
  * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
8649
8792
  */
8650
8793
  /** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
8651
- const FALLBACK_VERSION = "0.5.2";
8794
+ const FALLBACK_VERSION = "0.6.0";
8652
8795
  let cached;
8653
8796
  /** 本插件版本(如 `0.1.26`)。 */
8654
8797
  function pluginVersion() {
@@ -9199,6 +9342,19 @@ function apply(ctx, config = {}) {
9199
9342
  logger.info?.(`deepseek-web: 传输层=${transportState.effective}` + (transportState.degraded ? "(配置要求 Chromium,但本环境没有 electron.net.fetch,已降级为 Node)" : ""));
9200
9343
  let lastAutoSwitchAt = Date.now();
9201
9344
  /**
9345
+ * 上一次**真正换过号**的时刻(自动或手动),0 = 本次启动还没换过。
9346
+ *
9347
+ * 🔴 为什么不能用上面那个 `lastAutoSwitchAt` 代替:它在启动时被初始化成"启动时刻"
9348
+ * (`isSwitchDue` 要的是"从启动算起过了多久")。而限流换号有自己的**冷却**(距上次换号
9349
+ * 至少 3 分钟),若拿"启动时刻"当"上次换号",就会**每次重启后都开出一段"限流也不换号"
9350
+ * 的窗口** —— 实测 2026-09-27:插件 10:56:30 启动、10:57:54 撞限流,距启动只有 73 秒
9351
+ * ⇒ 被自己的冷却挡掉,用户看到的是"依旧没有自动切换账号"。
9352
+ * 用它之后语义才对:**没换过号 ⇒ 不套冷却**(限流是明确的坏状态,值得立刻换走)。
9353
+ *
9354
+ * 只在**换号成功**(含手动切号)时更新;探活失败、换号抛错都不算换过号。
9355
+ */
9356
+ let lastSwitchedAt = 0;
9357
+ /**
9202
9358
  * 最近一次**自动**换号的记录(只给界面显示用,不落盘)。
9203
9359
  *
9204
9360
  * 只记自动换号:手动切号是用户自己的操作,界面不需要"提示"他刚做过什么。
@@ -9230,7 +9386,8 @@ function apply(ctx, config = {}) {
9230
9386
  now: Date.now(),
9231
9387
  accounts,
9232
9388
  currentId: activeAccountId(),
9233
- throttledAt: throttleAt
9389
+ throttledAt: throttleAt,
9390
+ lastSwitchedAt
9234
9391
  });
9235
9392
  if (decision.action !== "switch") return;
9236
9393
  autoSwitching = true;
@@ -9250,6 +9407,7 @@ function apply(ctx, config = {}) {
9250
9407
  const fromId = activeAccountId();
9251
9408
  if (!setActiveAccount(decision.nextId)) return;
9252
9409
  lastAutoSwitchAt = Date.now();
9410
+ lastSwitchedAt = Date.now();
9253
9411
  const fromRecord = fromId ? readAccount(fromId) : void 0;
9254
9412
  lastAutoSwitch = {
9255
9413
  at: lastAutoSwitchAt,
@@ -9281,7 +9439,7 @@ function apply(ctx, config = {}) {
9281
9439
  const now = Date.now();
9282
9440
  if (kind === "throttled" && !isThrottleSwitchAllowed({
9283
9441
  throttledAt: now,
9284
- lastSwitchAt: lastAutoSwitchAt,
9442
+ lastSwitchAt: lastSwitchedAt,
9285
9443
  now
9286
9444
  })) return false;
9287
9445
  const fresh = freshThrottledIds(throttleAt, now);
@@ -9720,6 +9878,7 @@ function apply(ctx, config = {}) {
9720
9878
  }
9721
9879
  logger.info?.(`deepseek-web: 当前账号已切换为 ${id}`);
9722
9880
  lastAutoSwitchAt = Date.now();
9881
+ lastSwitchedAt = Date.now();
9723
9882
  sendJson(res, 200, {
9724
9883
  ok: true,
9725
9884
  activeId: id