dsh-approval-consult 1.1.2 → 1.1.4

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.2**, 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.2).
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).
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.2.tgz` from
64
+ 1. Download `dsh-approval-consult-1.1.4.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.2**,已发布到 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.2)。
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)。
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.2.tgz`;
52
+ `dsh-approval-consult-1.1.4.tgz`;
53
53
  2. 打开 DSH Web → 左侧「插件」→「添加插件」→「安装第三方插件」,填这个 tarball 的绝对路径,
54
54
  或直接填下载链接
55
- `https://github.com/0tests/dsh-approval-consult/releases/download/v1.1.2/dsh-approval-consult-1.1.2.tgz`;
55
+ `https://github.com/0tests/dsh-approval-consult/releases/download/v1.1.4/dsh-approval-consult-1.1.4.tgz`;
56
56
  3. 安装完成后点「立即启用」,重启一次 `dsh web` 更稳。
57
57
 
58
58
  ### 方式 C:本地目录(源码 / 开发)
package/lib/impl.js CHANGED
@@ -974,7 +974,9 @@ export function apply(ctx, config, runtime) {
974
974
  typeof review.decision?.reason === 'string' &&
975
975
  review.decision.reason.length >= 12 &&
976
976
  reason.includes(review.decision.reason)
977
- // 参数已知但不一致、或两者都无法核对:一律不复用(宁可缺材料,不展示错误结论)。
977
+ // 参数已知且不一致:无论理由文字是否命中都不复用——理由文本不能替代调用身份。
978
+ if (argsKnown && !sameArgs) continue
979
+ // 参数未知时,才退化为「理由原样命中」这一弱证据;两者都无法核对则不复用。
978
980
  if (!sameArgs && !reasonHit) continue
979
981
  let score = 1
980
982
  if (reasonHit) score += 4
@@ -1652,6 +1654,11 @@ export function apply(ctx, config, runtime) {
1652
1654
  const controller = new AbortController()
1653
1655
  const timer = setTimeout(() => controller.abort(new Error('advisory review timeout')), 90_000)
1654
1656
  review.classifyController = controller
1657
+ // 每次尝试使用独立缓冲:上一次失败留下的半截输出/错误不能污染本次结果。
1658
+ let text = ''
1659
+ let error = null
1660
+ let usage = null
1661
+ let finishKind = null
1655
1662
  const options = {
1656
1663
  ...review.options,
1657
1664
  maxTokens: 1800,
@@ -1662,23 +1669,28 @@ export function apply(ctx, config, runtime) {
1662
1669
  try {
1663
1670
  for await (const chunk of ctx.llm.stream(options)) {
1664
1671
  if (!chunk || typeof chunk !== 'object') continue
1665
- if (chunk.type === 'text-delta' && typeof chunk.text === 'string') review.text += chunk.text
1666
- else if (chunk.type === 'block-end' && chunk.block?.type === 'text' && typeof chunk.block.text === 'string' && review.text.length === 0) review.text = chunk.block.text
1667
- else if (chunk.type === 'usage') review.usage = chunk.usage ?? null
1672
+ if (chunk.type === 'text-delta' && typeof chunk.text === 'string') text += chunk.text
1673
+ else if (chunk.type === 'block-end' && chunk.block?.type === 'text' && typeof chunk.block.text === 'string' && text.length === 0) text = chunk.block.text
1674
+ else if (chunk.type === 'usage') usage = chunk.usage ?? null
1668
1675
  else if (chunk.type === 'finish') {
1669
- review.finishKind = chunk.reason?.kind ?? null
1670
- if (chunk.reason?.kind === 'error' || chunk.reason?.kind === 'aborted') review.error = messageOf(chunk.reason.failure)
1676
+ finishKind = chunk.reason?.kind ?? null
1677
+ if (chunk.reason?.kind === 'error' || chunk.reason?.kind === 'aborted') error = messageOf(chunk.reason.failure)
1671
1678
  }
1672
1679
  }
1673
- review.decisionText = review.text.trim()
1680
+ review.text = text
1681
+ review.decisionText = text.trim()
1674
1682
  review.decision = parseReviewDecision(review.decisionText)
1675
- if (review.decision === null && review.error === null) {
1676
- review.error = `审查输出无法解析:${clip(review.decisionText, 200)}`
1683
+ if (review.decision === null && error === null) {
1684
+ error = `审查输出无法解析:${clip(review.decisionText, 200)}`
1677
1685
  }
1678
- } catch (error) {
1679
- review.error = friendlySideCallError(error)
1680
- noteError('advisory-classify', error)
1686
+ } catch (thrown) {
1687
+ review.text = text
1688
+ error = friendlySideCallError(thrown)
1689
+ noteError('advisory-classify', thrown)
1681
1690
  } finally {
1691
+ review.error = error
1692
+ review.usage = usage
1693
+ review.finishKind = finishKind
1682
1694
  clearTimeout(timer)
1683
1695
  state.sideOptions.delete(options)
1684
1696
  review.classifyController = null
@@ -1821,7 +1833,8 @@ export function apply(ctx, config, runtime) {
1821
1833
  const key = typeof input?.consultKey === 'string' ? input.consultKey : ''
1822
1834
  if (key.length > 0) {
1823
1835
  const direct = state.pendings.get(key)
1824
- if (direct !== undefined) return direct
1836
+ // 命中缓存也要同步状态:重建记录可能在别处已经被允许/拒绝(安全复查 2026-11-04)。
1837
+ if (direct !== undefined) return refreshReconstructed(direct)
1825
1838
  }
1826
1839
  const sessionId = input?.sessionId === undefined || input.sessionId === null ? '' : String(input.sessionId)
1827
1840
  if (sessionId.length === 0) return null
@@ -1830,56 +1843,56 @@ export function apply(ctx, config, runtime) {
1830
1843
  const keys = state.pendingBySession.get(sessionId)
1831
1844
  if (!Array.isArray(keys) || keys.length === 0) {
1832
1845
  const rebuilt = reconstructPending(sessionId, callId.length > 0 ? callId : undefined, toolName.length > 0 ? toolName : undefined)
1833
- if (rebuilt !== null) refreshReconstructed(rebuilt)
1834
- return rebuilt
1846
+ return rebuilt === null ? null : refreshReconstructed(rebuilt)
1835
1847
  }
1836
1848
  for (let index = keys.length - 1; index >= 0; index -= 1) {
1837
1849
  const record = state.pendings.get(keys[index])
1838
1850
  if (record === undefined || record.resolvedAt !== null) continue
1839
1851
  if (callId.length > 0) {
1840
- if (record.callId === callId) return record
1852
+ if (record.callId === callId) return refreshReconstructed(record)
1841
1853
  continue
1842
1854
  }
1843
1855
  if (toolName.length > 0) {
1844
- if (record.toolName === toolName) return record
1856
+ if (record.toolName === toolName) return refreshReconstructed(record)
1845
1857
  continue
1846
1858
  }
1847
- return record
1859
+ return refreshReconstructed(record)
1848
1860
  }
1849
1861
  for (let index = keys.length - 1; index >= 0; index -= 1) {
1850
1862
  const record = state.pendings.get(keys[index])
1851
1863
  if (record === undefined) continue
1852
1864
  if (callId.length > 0 && record.callId !== callId) continue
1853
1865
  if (callId.length === 0 && toolName.length > 0 && record.toolName !== toolName) continue
1854
- return record
1866
+ return refreshReconstructed(record)
1855
1867
  }
1856
1868
  // 记录可能因插件热重载/重启丢失:从会话日志里的 approval/asked(尚无 approval/decided)重建。
1857
1869
  const rebuilt = reconstructPending(sessionId, callId.length > 0 ? callId : undefined, toolName.length > 0 ? toolName : undefined)
1858
- if (rebuilt !== null) refreshReconstructed(rebuilt)
1859
- return rebuilt
1870
+ return rebuilt === null ? null : refreshReconstructed(rebuilt)
1860
1871
  }
1861
1872
 
1862
1873
  /**
1863
1874
  * 重建出来的记录没有连接到 approval/request 的 next(),需要自己看日志确认是否已经有结果,
1864
- * 否则用户一旦在别处决定,面板会一直停在“等待审批”。
1875
+ * 否则用户一旦在别处决定,面板会一直停在“等待审批”(安全复查 2026-11-04)。
1876
+ * 命中结果时统一走 finalizePending,确保中止侧调用、失效图片引用等清理一致。
1865
1877
  */
1866
1878
  function refreshReconstructed(record) {
1867
- if (record.reconstructed !== true || record.resolvedAt !== null) return
1868
- if (record.approvalId === undefined) return
1879
+ if (record === null || record === undefined) return record
1880
+ if (record.reconstructed !== true || record.resolvedAt !== null) return record
1881
+ if (record.approvalId === undefined) return record
1869
1882
  try {
1870
1883
  const agent = liveAgentBySessionId(record.sessionId)
1871
- if (agent === null || typeof agent.session?.snapshotEvents !== 'function') return
1884
+ if (agent === null || typeof agent.session?.snapshotEvents !== 'function') return record
1872
1885
  const events = agent.session.snapshotEvents()
1873
1886
  for (const event of events) {
1874
1887
  if (event?.type !== 'approval/decided') continue
1875
1888
  if (event.data?.id === undefined || String(event.data.id) !== record.approvalId) continue
1876
- record.resolvedAt = Date.now()
1877
- record.outcome = typeof event.data?.outcome === 'string' ? event.data.outcome : null
1878
- return
1889
+ finalizePending(record, typeof event.data?.outcome === 'string' ? event.data.outcome : null, null)
1890
+ break
1879
1891
  }
1880
- } catch {
1881
- /* ignore */
1892
+ } catch (error) {
1893
+ noteError('refresh-reconstructed', error)
1882
1894
  }
1895
+ return record
1883
1896
  }
1884
1897
 
1885
1898
  /**
@@ -2487,6 +2500,11 @@ export function apply(ctx, config, runtime) {
2487
2500
  if (item === undefined || item.consultKey !== consultKey || Date.now() > item.expiresAt) {
2488
2501
  throw httpError(404, 'attachment-unknown', '这张图片已经失效,请重新上传。')
2489
2502
  }
2503
+ // 审批结束后图片引用必须立即失效(finalizePending 会清理,这里兜底防止竞态残留)。
2504
+ const pendingForImage = state.pendings.get(consultKey)
2505
+ if (pendingForImage === undefined || pendingForImage.resolvedAt !== null) {
2506
+ throw httpError(404, 'attachment-unknown', '这张图片已经失效,请重新上传。')
2507
+ }
2490
2508
  if (attachmentStore === null || typeof attachmentStore.readImage !== 'function') {
2491
2509
  throw httpError(409, 'attachment-unavailable', '当前环境没有可用的附件服务。')
2492
2510
  }
@@ -2524,6 +2542,10 @@ export function apply(ctx, config, runtime) {
2524
2542
  try {
2525
2543
  const data = await readBinaryBody(req, limit)
2526
2544
  if (data.length === 0) throw httpError(400, 'attachment-bad-request', '图片内容为空。')
2545
+ // 读取期间审批可能已经结束:结束后的图片一律不再登记(否则会“复活”已失效引用)。
2546
+ if (record.resolvedAt !== null) {
2547
+ throw httpError(409, 'approval-resolved', '这次审批已经结束,不能再上传图片。')
2548
+ }
2527
2549
  let ref = null
2528
2550
  try {
2529
2551
  ref = await attachmentStore.saveImage({
@@ -2534,6 +2556,11 @@ export function apply(ctx, config, runtime) {
2534
2556
  } catch (error) {
2535
2557
  throw mapAttachmentError(error)
2536
2558
  }
2559
+ // saveImage 也可能耗时:登记前再复核一次;若此时审批已结束,本次保存的宿主对象
2560
+ // 没有对应引用(宿主接口未提供删除语义),但绝不会被登记成可读取的附件。
2561
+ if (record.resolvedAt !== null) {
2562
+ throw httpError(409, 'approval-resolved', '这次审批已经结束,不能再上传图片。')
2563
+ }
2537
2564
  const item = registerAttachment(ref, record.key, record.sessionId)
2538
2565
  sendJson(res, 200, { ok: true, attachment: attachmentMeta(item) })
2539
2566
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-approval-consult",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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.",