dsh-approval-consult 1.1.3 → 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 +2 -2
- package/README.md +3 -3
- package/lib/impl.js +56 -25
- package/package.json +2 -2
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.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)
|
|
@@ -1833,7 +1838,8 @@ export function apply(ctx, config, runtime) {
|
|
|
1833
1838
|
const key = typeof input?.consultKey === 'string' ? input.consultKey : ''
|
|
1834
1839
|
if (key.length > 0) {
|
|
1835
1840
|
const direct = state.pendings.get(key)
|
|
1836
|
-
|
|
1841
|
+
// 命中缓存也要同步状态:重建记录可能在别处已经被允许/拒绝(安全复查 2026-11-04)。
|
|
1842
|
+
if (direct !== undefined) return refreshReconstructed(direct)
|
|
1837
1843
|
}
|
|
1838
1844
|
const sessionId = input?.sessionId === undefined || input.sessionId === null ? '' : String(input.sessionId)
|
|
1839
1845
|
if (sessionId.length === 0) return null
|
|
@@ -1842,56 +1848,60 @@ export function apply(ctx, config, runtime) {
|
|
|
1842
1848
|
const keys = state.pendingBySession.get(sessionId)
|
|
1843
1849
|
if (!Array.isArray(keys) || keys.length === 0) {
|
|
1844
1850
|
const rebuilt = reconstructPending(sessionId, callId.length > 0 ? callId : undefined, toolName.length > 0 ? toolName : undefined)
|
|
1845
|
-
|
|
1846
|
-
return rebuilt
|
|
1851
|
+
return rebuilt === null ? null : refreshReconstructed(rebuilt)
|
|
1847
1852
|
}
|
|
1848
1853
|
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
1849
1854
|
const record = state.pendings.get(keys[index])
|
|
1850
1855
|
if (record === undefined || record.resolvedAt !== null) continue
|
|
1851
1856
|
if (callId.length > 0) {
|
|
1852
|
-
if (record.callId === callId) return record
|
|
1857
|
+
if (record.callId === callId) return refreshReconstructed(record)
|
|
1853
1858
|
continue
|
|
1854
1859
|
}
|
|
1855
1860
|
if (toolName.length > 0) {
|
|
1856
|
-
if (record.toolName === toolName) return record
|
|
1861
|
+
if (record.toolName === toolName) return refreshReconstructed(record)
|
|
1857
1862
|
continue
|
|
1858
1863
|
}
|
|
1859
|
-
return record
|
|
1864
|
+
return refreshReconstructed(record)
|
|
1860
1865
|
}
|
|
1861
1866
|
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
1862
1867
|
const record = state.pendings.get(keys[index])
|
|
1863
1868
|
if (record === undefined) continue
|
|
1864
1869
|
if (callId.length > 0 && record.callId !== callId) continue
|
|
1865
1870
|
if (callId.length === 0 && toolName.length > 0 && record.toolName !== toolName) continue
|
|
1866
|
-
return record
|
|
1871
|
+
return refreshReconstructed(record)
|
|
1867
1872
|
}
|
|
1868
1873
|
// 记录可能因插件热重载/重启丢失:从会话日志里的 approval/asked(尚无 approval/decided)重建。
|
|
1869
1874
|
const rebuilt = reconstructPending(sessionId, callId.length > 0 ? callId : undefined, toolName.length > 0 ? toolName : undefined)
|
|
1870
|
-
|
|
1871
|
-
return rebuilt
|
|
1875
|
+
return rebuilt === null ? null : refreshReconstructed(rebuilt)
|
|
1872
1876
|
}
|
|
1873
1877
|
|
|
1874
1878
|
/**
|
|
1875
1879
|
* 重建出来的记录没有连接到 approval/request 的 next(),需要自己看日志确认是否已经有结果,
|
|
1876
|
-
*
|
|
1880
|
+
* 否则用户一旦在别处决定,面板会一直停在“等待审批”(安全复查 2026-11-04)。
|
|
1881
|
+
* 命中结果时统一走 finalizePending,确保中止侧调用、失效图片引用等清理一致。
|
|
1877
1882
|
*/
|
|
1878
|
-
function refreshReconstructed(record) {
|
|
1879
|
-
if (record
|
|
1880
|
-
if (record.
|
|
1883
|
+
function refreshReconstructed(record, decisionsBySession) {
|
|
1884
|
+
if (record === null || record === undefined) return record
|
|
1885
|
+
if (record.reconstructed !== true || record.resolvedAt !== null) return record
|
|
1886
|
+
if (record.approvalId === undefined) return record
|
|
1881
1887
|
try {
|
|
1882
|
-
|
|
1883
|
-
if (
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1900
|
+
if (decisions.has(record.approvalId)) finalizePending(record, decisions.get(record.approvalId), null)
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
noteError('refresh-reconstructed', error)
|
|
1894
1903
|
}
|
|
1904
|
+
return record
|
|
1895
1905
|
}
|
|
1896
1906
|
|
|
1897
1907
|
/**
|
|
@@ -2234,6 +2244,7 @@ export function apply(ctx, config, runtime) {
|
|
|
2234
2244
|
const jobId = typeof query.jobId === 'string' ? query.jobId : ''
|
|
2235
2245
|
const job = state.jobs.get(jobId)
|
|
2236
2246
|
if (job === undefined) throw httpError(404, 'job-not-found', '这次回答已经过期或不存在。')
|
|
2247
|
+
refreshReconstructed(state.pendings.get(job.consultKey))
|
|
2237
2248
|
const cursor = intIn(query.cursor, 0, 0, Number.MAX_SAFE_INTEGER)
|
|
2238
2249
|
const safeCursor = Math.min(cursor, job.text.length)
|
|
2239
2250
|
return {
|
|
@@ -2500,7 +2511,7 @@ export function apply(ctx, config, runtime) {
|
|
|
2500
2511
|
throw httpError(404, 'attachment-unknown', '这张图片已经失效,请重新上传。')
|
|
2501
2512
|
}
|
|
2502
2513
|
// 审批结束后图片引用必须立即失效(finalizePending 会清理,这里兜底防止竞态残留)。
|
|
2503
|
-
const pendingForImage = state.pendings.get(consultKey)
|
|
2514
|
+
const pendingForImage = refreshReconstructed(state.pendings.get(consultKey))
|
|
2504
2515
|
if (pendingForImage === undefined || pendingForImage.resolvedAt !== null) {
|
|
2505
2516
|
throw httpError(404, 'attachment-unknown', '这张图片已经失效,请重新上传。')
|
|
2506
2517
|
}
|
|
@@ -2513,6 +2524,11 @@ export function apply(ctx, config, runtime) {
|
|
|
2513
2524
|
} catch (error) {
|
|
2514
2525
|
throw mapAttachmentError(error)
|
|
2515
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
|
+
}
|
|
2516
2532
|
sendImage(res, stored.data, stored.ref?.mediaType ?? item.ref.mediaType)
|
|
2517
2533
|
return
|
|
2518
2534
|
}
|
|
@@ -2542,6 +2558,7 @@ export function apply(ctx, config, runtime) {
|
|
|
2542
2558
|
const data = await readBinaryBody(req, limit)
|
|
2543
2559
|
if (data.length === 0) throw httpError(400, 'attachment-bad-request', '图片内容为空。')
|
|
2544
2560
|
// 读取期间审批可能已经结束:结束后的图片一律不再登记(否则会“复活”已失效引用)。
|
|
2561
|
+
refreshReconstructed(record)
|
|
2545
2562
|
if (record.resolvedAt !== null) {
|
|
2546
2563
|
throw httpError(409, 'approval-resolved', '这次审批已经结束,不能再上传图片。')
|
|
2547
2564
|
}
|
|
@@ -2557,6 +2574,7 @@ export function apply(ctx, config, runtime) {
|
|
|
2557
2574
|
}
|
|
2558
2575
|
// saveImage 也可能耗时:登记前再复核一次;若此时审批已结束,本次保存的宿主对象
|
|
2559
2576
|
// 没有对应引用(宿主接口未提供删除语义),但绝不会被登记成可读取的附件。
|
|
2577
|
+
refreshReconstructed(record)
|
|
2560
2578
|
if (record.resolvedAt !== null) {
|
|
2561
2579
|
throw httpError(409, 'approval-resolved', '这次审批已经结束,不能再上传图片。')
|
|
2562
2580
|
}
|
|
@@ -3452,6 +3470,14 @@ export function apply(ctx, config, runtime) {
|
|
|
3452
3470
|
}
|
|
3453
3471
|
}
|
|
3454
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))
|
|
3455
3481
|
|
|
3456
3482
|
const sweepTimer = setInterval(() => {
|
|
3457
3483
|
try {
|
|
@@ -3524,6 +3550,11 @@ export function apply(ctx, config, runtime) {
|
|
|
3524
3550
|
}
|
|
3525
3551
|
}
|
|
3526
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
|
+
}
|
|
3527
3558
|
for (const job of state.jobs.values()) {
|
|
3528
3559
|
if (job.status !== 'streaming') continue
|
|
3529
3560
|
job.status = 'cancelled'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-approval-consult",
|
|
3
|
-
"version": "1.1.
|
|
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": {
|