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 +2 -2
- package/README.md +3 -3
- package/lib/impl.js +57 -30
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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')
|
|
1666
|
-
else if (chunk.type === 'block-end' && chunk.block?.type === 'text' && typeof chunk.block.text === 'string' &&
|
|
1667
|
-
else if (chunk.type === 'usage')
|
|
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
|
-
|
|
1670
|
-
if (chunk.reason?.kind === 'error' || chunk.reason?.kind === 'aborted')
|
|
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.
|
|
1680
|
+
review.text = text
|
|
1681
|
+
review.decisionText = text.trim()
|
|
1674
1682
|
review.decision = parseReviewDecision(review.decisionText)
|
|
1675
|
-
if (review.decision === null &&
|
|
1676
|
-
|
|
1683
|
+
if (review.decision === null && error === null) {
|
|
1684
|
+
error = `审查输出无法解析:${clip(review.decisionText, 200)}`
|
|
1677
1685
|
}
|
|
1678
|
-
} catch (
|
|
1679
|
-
review.
|
|
1680
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1868
|
-
if (record.
|
|
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.
|
|
1877
|
-
|
|
1878
|
-
return
|
|
1889
|
+
finalizePending(record, typeof event.data?.outcome === 'string' ? event.data.outcome : null, null)
|
|
1890
|
+
break
|
|
1879
1891
|
}
|
|
1880
|
-
} catch {
|
|
1881
|
-
|
|
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.
|
|
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.",
|