dsh-approval-consult 1.1.4 → 1.1.5

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/README.en.md CHANGED
@@ -53,7 +53,7 @@ notes or reject blindly. This plugin adds a consultation panel on top of the app
53
53
 
54
54
  Requires DSH Web `0.2.1-rc.2` (tested) and Node `>=20`.
55
55
 
56
- Current version **v1.1.4**, published to npm ([`dsh-approval-consult`](https://www.npmjs.com/package/dsh-approval-consult)) and as a [GitHub release](https://github.com/0tests/dsh-approval-consult/releases/tag/v1.1.4).
56
+ Current version **v1.1.5**, published to npm ([`dsh-approval-consult`](https://www.npmjs.com/package/dsh-approval-consult)) and as a [GitHub release](https://github.com/0tests/dsh-approval-consult/releases/tag/v1.1.5).
57
57
 
58
58
  ```bash
59
59
  npm install dsh-approval-consult
@@ -61,7 +61,7 @@ npm install dsh-approval-consult
61
61
 
62
62
  Or install the release tarball:
63
63
 
64
- 1. Download `dsh-approval-consult-1.1.4.tgz` from
64
+ 1. Download `dsh-approval-consult-1.1.5.tgz` from
65
65
  [Releases](https://github.com/0tests/dsh-approval-consult/releases/latest).
66
66
  2. Open DSH Web → **Plugins** → **Add plugin** → **Install third-party plugin** and point it at the
67
67
  tarball's absolute path (or the download URL directly).
package/README.md CHANGED
@@ -37,7 +37,7 @@ Auto 审查(`@deepseek-ai/dsh-experimental-auto-review`)会在工具执行
37
37
 
38
38
  ## 安装
39
39
 
40
- > 当前版本 **v1.1.4**,已发布到 npm([`dsh-approval-consult`](https://www.npmjs.com/package/dsh-approval-consult))和 [GitHub Release](https://github.com/0tests/dsh-approval-consult/releases/tag/v1.1.4)。
40
+ > 当前版本 **v1.1.5**,已发布到 npm([`dsh-approval-consult`](https://www.npmjs.com/package/dsh-approval-consult))和 [GitHub Release](https://github.com/0tests/dsh-approval-consult/releases/tag/v1.1.5)。
41
41
  > `npm install dsh-approval-consult`,或让 Agent 用 `plugin_manager` 安装这个包名;下面也保留了 tarball / 本地目录两种方式。
42
42
 
43
43
  ### 方式 A:npm
@@ -49,10 +49,10 @@ Auto 审查(`@deepseek-ai/dsh-experimental-auto-review`)会在工具执行
49
49
  ### 方式 B:从 Release 安装(tarball)
50
50
 
51
51
  1. 在 [Releases](https://github.com/0tests/dsh-approval-consult/releases/latest) 下载
52
- `dsh-approval-consult-1.1.4.tgz`;
52
+ `dsh-approval-consult-1.1.5.tgz`;
53
53
  2. 打开 DSH Web → 左侧「插件」→「添加插件」→「安装第三方插件」,填这个 tarball 的绝对路径,
54
54
  或直接填下载链接
55
- `https://github.com/0tests/dsh-approval-consult/releases/download/v1.1.4/dsh-approval-consult-1.1.4.tgz`;
55
+ `https://github.com/0tests/dsh-approval-consult/releases/download/v1.1.5/dsh-approval-consult-1.1.5.tgz`;
56
56
  3. 安装完成后点「立即启用」,重启一次 `dsh web` 更稳。
57
57
 
58
58
  ### 方式 C:本地目录(源码 / 开发)
package/lib/impl.js CHANGED
@@ -1278,6 +1278,7 @@ export function apply(ctx, config, runtime) {
1278
1278
 
1279
1279
  /* 上面的 await 会让出执行权:必须在不再 await 的临界段里重新核对审批状态与并发名额,
1280
1280
  否则多个并发请求会一起通过数量检查(安全审查 2026-11-03)。 */
1281
+ refreshReconstructed(record)
1281
1282
  if (record.resolvedAt !== null) {
1282
1283
  throw httpError(409, 'approval-resolved', '这次审批已经结束了,不能继续提问。')
1283
1284
  }
@@ -1394,6 +1395,8 @@ export function apply(ctx, config, runtime) {
1394
1395
  entry.answer = job.text
1395
1396
  entry.truncated = finishKind === 'length'
1396
1397
  entry.usage = job.usage
1398
+ // 模型可能在后台轮询前就结束:提交结果/自动重试前同步审批。
1399
+ refreshReconstructed(record)
1397
1400
  if (job.controller.signal.aborted) {
1398
1401
  job.status = 'cancelled'
1399
1402
  entry.status = 'cancelled'
@@ -1677,6 +1680,8 @@ export function apply(ctx, config, runtime) {
1677
1680
  if (chunk.reason?.kind === 'error' || chunk.reason?.kind === 'aborted') error = messageOf(chunk.reason.failure)
1678
1681
  }
1679
1682
  }
1683
+ refreshReconstructed(record)
1684
+ if (controller.signal.aborted) throw controller.signal.reason ?? new Error('review cancelled')
1680
1685
  review.text = text
1681
1686
  review.decisionText = text.trim()
1682
1687
  review.decision = parseReviewDecision(review.decisionText)
@@ -1875,20 +1880,24 @@ export function apply(ctx, config, runtime) {
1875
1880
  * 否则用户一旦在别处决定,面板会一直停在“等待审批”(安全复查 2026-11-04)。
1876
1881
  * 命中结果时统一走 finalizePending,确保中止侧调用、失效图片引用等清理一致。
1877
1882
  */
1878
- function refreshReconstructed(record) {
1883
+ function refreshReconstructed(record, decisionsBySession) {
1879
1884
  if (record === null || record === undefined) return record
1880
1885
  if (record.reconstructed !== true || record.resolvedAt !== null) return record
1881
1886
  if (record.approvalId === undefined) return record
1882
1887
  try {
1883
- const agent = liveAgentBySessionId(record.sessionId)
1884
- if (agent === null || typeof agent.session?.snapshotEvents !== 'function') return record
1885
- const events = agent.session.snapshotEvents()
1886
- for (const event of events) {
1887
- if (event?.type !== 'approval/decided') continue
1888
- if (event.data?.id === undefined || String(event.data.id) !== record.approvalId) continue
1889
- finalizePending(record, typeof event.data?.outcome === 'string' ? event.data.outcome : null, null)
1890
- break
1888
+ let decisions = decisionsBySession?.get(record.sessionId)
1889
+ if (decisions === undefined) {
1890
+ const agent = liveAgentBySessionId(record.sessionId)
1891
+ if (agent === null || typeof agent.session?.snapshotEvents !== 'function') return record
1892
+ decisions = new Map()
1893
+ for (const event of agent.session.snapshotEvents()) {
1894
+ if (event?.type !== 'approval/decided' || event.data?.id === undefined) continue
1895
+ decisions.set(String(event.data.id), typeof event.data?.outcome === 'string' ? event.data.outcome : null)
1896
+ }
1897
+ // 后台一轮检查中,同一会话只读取和索引一次日志。
1898
+ decisionsBySession?.set(record.sessionId, decisions)
1891
1899
  }
1900
+ if (decisions.has(record.approvalId)) finalizePending(record, decisions.get(record.approvalId), null)
1892
1901
  } catch (error) {
1893
1902
  noteError('refresh-reconstructed', error)
1894
1903
  }
@@ -2235,6 +2244,7 @@ export function apply(ctx, config, runtime) {
2235
2244
  const jobId = typeof query.jobId === 'string' ? query.jobId : ''
2236
2245
  const job = state.jobs.get(jobId)
2237
2246
  if (job === undefined) throw httpError(404, 'job-not-found', '这次回答已经过期或不存在。')
2247
+ refreshReconstructed(state.pendings.get(job.consultKey))
2238
2248
  const cursor = intIn(query.cursor, 0, 0, Number.MAX_SAFE_INTEGER)
2239
2249
  const safeCursor = Math.min(cursor, job.text.length)
2240
2250
  return {
@@ -2501,7 +2511,7 @@ export function apply(ctx, config, runtime) {
2501
2511
  throw httpError(404, 'attachment-unknown', '这张图片已经失效,请重新上传。')
2502
2512
  }
2503
2513
  // 审批结束后图片引用必须立即失效(finalizePending 会清理,这里兜底防止竞态残留)。
2504
- const pendingForImage = state.pendings.get(consultKey)
2514
+ const pendingForImage = refreshReconstructed(state.pendings.get(consultKey))
2505
2515
  if (pendingForImage === undefined || pendingForImage.resolvedAt !== null) {
2506
2516
  throw httpError(404, 'attachment-unknown', '这张图片已经失效,请重新上传。')
2507
2517
  }
@@ -2514,6 +2524,11 @@ export function apply(ctx, config, runtime) {
2514
2524
  } catch (error) {
2515
2525
  throw mapAttachmentError(error)
2516
2526
  }
2527
+ // readImage 等待期间也可能结束审批或撤销引用,发送字节前再次核对。
2528
+ refreshReconstructed(pendingForImage)
2529
+ if (pendingForImage.resolvedAt !== null || state.attachments.get(id) !== item || Date.now() > item.expiresAt) {
2530
+ throw httpError(404, 'attachment-unknown', '这张图片已经失效,请重新上传。')
2531
+ }
2517
2532
  sendImage(res, stored.data, stored.ref?.mediaType ?? item.ref.mediaType)
2518
2533
  return
2519
2534
  }
@@ -2543,6 +2558,7 @@ export function apply(ctx, config, runtime) {
2543
2558
  const data = await readBinaryBody(req, limit)
2544
2559
  if (data.length === 0) throw httpError(400, 'attachment-bad-request', '图片内容为空。')
2545
2560
  // 读取期间审批可能已经结束:结束后的图片一律不再登记(否则会“复活”已失效引用)。
2561
+ refreshReconstructed(record)
2546
2562
  if (record.resolvedAt !== null) {
2547
2563
  throw httpError(409, 'approval-resolved', '这次审批已经结束,不能再上传图片。')
2548
2564
  }
@@ -2558,6 +2574,7 @@ export function apply(ctx, config, runtime) {
2558
2574
  }
2559
2575
  // saveImage 也可能耗时:登记前再复核一次;若此时审批已结束,本次保存的宿主对象
2560
2576
  // 没有对应引用(宿主接口未提供删除语义),但绝不会被登记成可读取的附件。
2577
+ refreshReconstructed(record)
2561
2578
  if (record.resolvedAt !== null) {
2562
2579
  throw httpError(409, 'approval-resolved', '这次审批已经结束,不能再上传图片。')
2563
2580
  }
@@ -3453,6 +3470,14 @@ export function apply(ctx, config, runtime) {
3453
3470
  }
3454
3471
  }
3455
3472
 
3473
+ // 重建记录没有原审批的完成回调:即使面板关闭,也要取消侧调用并失效附件。
3474
+ // 后台延迟为一次轮询间隔(正常调度下约 1 秒);敏感路由另做即时复核。
3475
+ const reconstructedTimer = setInterval(() => {
3476
+ const decisionsBySession = new Map()
3477
+ for (const record of state.pendings.values()) refreshReconstructed(record, decisionsBySession)
3478
+ }, 1000)
3479
+ if (typeof reconstructedTimer.unref === 'function') reconstructedTimer.unref()
3480
+ addDisposer(() => clearInterval(reconstructedTimer))
3456
3481
 
3457
3482
  const sweepTimer = setInterval(() => {
3458
3483
  try {
@@ -3525,6 +3550,11 @@ export function apply(ctx, config, runtime) {
3525
3550
  }
3526
3551
  }
3527
3552
  disposers.length = 0
3553
+ for (const record of state.pendings.values()) {
3554
+ for (const review of [record.review, record.advisoryReview]) {
3555
+ review?.classifyController?.abort(new Error('approval-consult unloaded'))
3556
+ }
3557
+ }
3528
3558
  for (const job of state.jobs.values()) {
3529
3559
  if (job.status !== 'streaming') continue
3530
3560
  job.status = 'cancelled'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-approval-consult",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Approval consultation panel for DeepSeek Harness: while a tool call waits for approval, ask the executor AI and the auto-review AI independently, in two isolated channels with prompt-cache reuse.",
@@ -34,7 +34,7 @@
34
34
  "NOTICE.md"
35
35
  ],
36
36
  "scripts": {
37
- "test": "node tests/unit.mjs && node tests/locale-parity.mjs && node tests/css-classes.mjs && node tests/security.mjs",
37
+ "test": "node tests/unit.mjs && node tests/locale-parity.mjs && node tests/css-classes.mjs && node tests/security.mjs && node tests/security-lifecycle.mjs",
38
38
  "check": "node --check lib/impl.js && node --check lib/client.js && node --check lib/index.js"
39
39
  },
40
40
  "engines": {