dsh-approval-consult 1.1.0 → 1.1.2
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 +9 -3
- package/README.md +14 -7
- package/lib/client.js +2 -0
- package/lib/impl.js +144 -74
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/0tests/dsh-approval-consult/actions/workflows/ci.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-approval-consult)
|
|
5
6
|
|
|
6
7
|
> **English** · [中文主 README](README.md)
|
|
7
8
|
|
|
@@ -52,10 +53,15 @@ notes or reject blindly. This plugin adds a consultation panel on top of the app
|
|
|
52
53
|
|
|
53
54
|
Requires DSH Web `0.2.1-rc.2` (tested) and Node `>=20`.
|
|
54
55
|
|
|
55
|
-
Current version **v1.1.
|
|
56
|
-
The npm package is not published yet — install the release tarball:
|
|
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).
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
```bash
|
|
59
|
+
npm install dsh-approval-consult
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or install the release tarball:
|
|
63
|
+
|
|
64
|
+
1. Download `dsh-approval-consult-1.1.2.tgz` from
|
|
59
65
|
[Releases](https://github.com/0tests/dsh-approval-consult/releases/latest).
|
|
60
66
|
2. Open DSH Web → **Plugins** → **Add plugin** → **Install third-party plugin** and point it at the
|
|
61
67
|
tarball's absolute path (or the download URL directly).
|
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/0tests/dsh-approval-consult/actions/workflows/ci.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-approval-consult)
|
|
5
6
|
|
|
6
7
|
> [English](README.en.md) · 中文
|
|
7
8
|
>
|
|
@@ -36,25 +37,31 @@ Auto 审查(`@deepseek-ai/dsh-experimental-auto-review`)会在工具执行
|
|
|
36
37
|
|
|
37
38
|
## 安装
|
|
38
39
|
|
|
39
|
-
> 当前版本 **v1.1.
|
|
40
|
-
> npm
|
|
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)。
|
|
41
|
+
> `npm install dsh-approval-consult`,或让 Agent 用 `plugin_manager` 安装这个包名;下面也保留了 tarball / 本地目录两种方式。
|
|
41
42
|
|
|
42
|
-
### 方式 A
|
|
43
|
+
### 方式 A:npm
|
|
44
|
+
|
|
45
|
+
1. `npm install dsh-approval-consult`(或让 Agent 用 `plugin_manager` 的 `install_bundle` 安装 npm 包名);
|
|
46
|
+
2. 在 DSH Web 的「插件」里启用;
|
|
47
|
+
3. 重启一次 `dsh web` 更稳。
|
|
48
|
+
|
|
49
|
+
### 方式 B:从 Release 安装(tarball)
|
|
43
50
|
|
|
44
51
|
1. 在 [Releases](https://github.com/0tests/dsh-approval-consult/releases/latest) 下载
|
|
45
|
-
`dsh-approval-consult-1.1.
|
|
52
|
+
`dsh-approval-consult-1.1.2.tgz`;
|
|
46
53
|
2. 打开 DSH Web → 左侧「插件」→「添加插件」→「安装第三方插件」,填这个 tarball 的绝对路径,
|
|
47
54
|
或直接填下载链接
|
|
48
|
-
`https://github.com/0tests/dsh-approval-consult/releases/download/v1.1.
|
|
55
|
+
`https://github.com/0tests/dsh-approval-consult/releases/download/v1.1.2/dsh-approval-consult-1.1.2.tgz`;
|
|
49
56
|
3. 安装完成后点「立即启用」,重启一次 `dsh web` 更稳。
|
|
50
57
|
|
|
51
|
-
### 方式
|
|
58
|
+
### 方式 C:本地目录(源码 / 开发)
|
|
52
59
|
|
|
53
60
|
1. `git clone https://github.com/0tests/dsh-approval-consult`;
|
|
54
61
|
2. 打开 DSH Web →「插件」→「添加插件」→「安装第三方插件」,输入该目录的绝对路径;
|
|
55
62
|
3. 安装完成后点「立即启用」。
|
|
56
63
|
|
|
57
|
-
### 方式
|
|
64
|
+
### 方式 D:CLI / 会话内
|
|
58
65
|
|
|
59
66
|
让 Agent 用 `plugin_manager` 的 `install_bundle` 安装该 tarball / 目录 / npm 包(需要审批权限)。
|
|
60
67
|
|
package/lib/client.js
CHANGED
|
@@ -960,6 +960,8 @@ window.__ModuleLoader__.load({
|
|
|
960
960
|
injectError: turn.injectError ?? null,
|
|
961
961
|
truncated: turn.truncated === true,
|
|
962
962
|
jobId: typeof turn.jobId === 'string' && turn.jobId.length > 0 ? turn.jobId : null,
|
|
963
|
+
/** 这条问答附带的图片元数据(/context 刷新后必须保留,否则缩略图会消失)。 */
|
|
964
|
+
attachments: Array.isArray(turn.attachments) ? turn.attachments : [],
|
|
963
965
|
}))
|
|
964
966
|
: []
|
|
965
967
|
return {
|
package/lib/impl.js
CHANGED
|
@@ -59,6 +59,14 @@ const SIDE_TOOLCALL_CORRECTION =
|
|
|
59
59
|
const INJECT_SOURCE_KIND = 'approval-consult'
|
|
60
60
|
const CONTEXT_SUMMARY_MAX_CHARS = 120
|
|
61
61
|
const MAX_BODY_BYTES = 64 * 1024
|
|
62
|
+
/** 插件版本(诊断用):从 package.json 读取,避免手写版本号漂移。 */
|
|
63
|
+
const PLUGIN_VERSION = (() => {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
|
|
66
|
+
} catch {
|
|
67
|
+
return '0.0.0'
|
|
68
|
+
}
|
|
69
|
+
})()
|
|
62
70
|
/** 图片上传请求体上限(再受 DSH 附件服务的 imageLimits 约束)。 */
|
|
63
71
|
const MAX_ATTACHMENT_BODY_BYTES = 8 * 1024 * 1024
|
|
64
72
|
/** 附件引用登记的有效期(审批结束后立即失效,这里是兜底)。 */
|
|
@@ -484,6 +492,8 @@ export function apply(ctx, config, runtime) {
|
|
|
484
492
|
jobs: new Map(),
|
|
485
493
|
/** uploadId -> 附件引用登记(图片只存引用,字节在 DSH 附件服务里) */
|
|
486
494
|
attachments: new Map(),
|
|
495
|
+
/** consultKey -> 进行中的上传预占数(先占名额再 saveImage,失败释放) */
|
|
496
|
+
attachmentPending: new Map(),
|
|
487
497
|
/** 侧调用 options 集合(对象身份),llm/stream 捕获器直接跳过 */
|
|
488
498
|
sideOptions: new WeakSet(),
|
|
489
499
|
/** 已被本插件包装/记录过的 options(避免根与 agent 两处监听重复处理) */
|
|
@@ -532,6 +542,7 @@ export function apply(ctx, config, runtime) {
|
|
|
532
542
|
state = previousState
|
|
533
543
|
// 瞬态字段一律重建;旧 jobs 已被上一代 dispose 中止。
|
|
534
544
|
state.jobs = new Map()
|
|
545
|
+
state.attachmentPending = new Map()
|
|
535
546
|
state.sideOptions = new WeakSet()
|
|
536
547
|
state.wrappedOptions = new WeakSet()
|
|
537
548
|
state.agentDisposers = new Map()
|
|
@@ -939,8 +950,9 @@ export function apply(ctx, config, runtime) {
|
|
|
939
950
|
}
|
|
940
951
|
|
|
941
952
|
/**
|
|
942
|
-
*
|
|
943
|
-
*
|
|
953
|
+
* 把这次审批和一条审查记录配对:只接受**可验证的关联**——参数完全一致,或审批理由里
|
|
954
|
+
* 原样包含审查理由。工具名相同不再构成匹配依据,也不跨 session 猜测(安全审查 2026-11-03)。
|
|
955
|
+
* 无法准确关联时返回 null,让面板显示「缺少审查材料」或对当前调用重新评估。
|
|
944
956
|
*/
|
|
945
957
|
function matchReview(sessionId, toolName, exec, reason) {
|
|
946
958
|
const list = state.reviews.get(sessionId)
|
|
@@ -954,45 +966,26 @@ export function apply(ctx, config, runtime) {
|
|
|
954
966
|
const action = review.action
|
|
955
967
|
if (action === null) continue
|
|
956
968
|
if (action.name !== toolName) continue
|
|
969
|
+
const reviewArgs = canonicalJson(normalizeArguments(action.arguments))
|
|
970
|
+
const argsKnown = execArgs !== undefined && reviewArgs.length > 0
|
|
971
|
+
const sameArgs = argsKnown && reviewArgs === execArgs
|
|
972
|
+
const reasonHit =
|
|
973
|
+
typeof reason === 'string' &&
|
|
974
|
+
typeof review.decision?.reason === 'string' &&
|
|
975
|
+
review.decision.reason.length >= 12 &&
|
|
976
|
+
reason.includes(review.decision.reason)
|
|
977
|
+
// 参数已知但不一致、或两者都无法核对:一律不复用(宁可缺材料,不展示错误结论)。
|
|
978
|
+
if (!sameArgs && !reasonHit) continue
|
|
957
979
|
let score = 1
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
if (execArgs !== undefined) {
|
|
962
|
-
const reviewArgs = canonicalJson(normalizeArguments(action.arguments))
|
|
963
|
-
if (reviewArgs.length > 0 && reviewArgs === execArgs) score += 3
|
|
964
|
-
}
|
|
980
|
+
if (reasonHit) score += 4
|
|
981
|
+
if (sameArgs) score += 3
|
|
965
982
|
if (review.decision !== null) score += 1
|
|
966
983
|
if (score > bestScore || (score === bestScore && best !== null && review.at > best.at)) {
|
|
967
984
|
best = review
|
|
968
985
|
bestScore = score
|
|
969
986
|
}
|
|
970
987
|
}
|
|
971
|
-
|
|
972
|
-
const sameTool = candidates.filter((review) => review.action?.name === toolName)
|
|
973
|
-
if (sameTool.length === 1) return sameTool[0]
|
|
974
|
-
// 跨 session 兜底:某些会话的审查请求可能被记到了别的 sessionId 上。
|
|
975
|
-
// 只接受高置信匹配(参数完全一致或审查理由原样出现在审批理由里),且必须唯一。
|
|
976
|
-
let crossMatch = null
|
|
977
|
-
for (const [otherSession, otherList] of state.reviews) {
|
|
978
|
-
if (otherSession === sessionId || !Array.isArray(otherList)) continue
|
|
979
|
-
for (const review of otherList) {
|
|
980
|
-
if (review.claimedBy !== null || review.error !== null) continue
|
|
981
|
-
if (review.action === null || review.action.name !== toolName) continue
|
|
982
|
-
if (Date.now() - review.at > 120_000) continue
|
|
983
|
-
const reviewArgs = canonicalJson(normalizeArguments(review.action.arguments))
|
|
984
|
-
const sameArgs = execArgs !== undefined && reviewArgs.length > 0 && reviewArgs === execArgs
|
|
985
|
-
const reasonHit =
|
|
986
|
-
typeof reason === 'string' &&
|
|
987
|
-
typeof review.decision?.reason === 'string' &&
|
|
988
|
-
review.decision.reason.length >= 12 &&
|
|
989
|
-
reason.includes(review.decision.reason)
|
|
990
|
-
if (!sameArgs && !reasonHit) continue
|
|
991
|
-
if (crossMatch !== null) return null
|
|
992
|
-
crossMatch = review
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
return crossMatch
|
|
988
|
+
return best
|
|
996
989
|
}
|
|
997
990
|
|
|
998
991
|
/** pending 结束:记录结果、中止仍在流式输出的侧调用。 */
|
|
@@ -1017,6 +1010,16 @@ export function apply(ctx, config, runtime) {
|
|
|
1017
1010
|
/* ignore */
|
|
1018
1011
|
}
|
|
1019
1012
|
}
|
|
1013
|
+
// 审批结束:中止仍在进行的「重新评估」,避免审批结束后继续计费(安全审查 2026-11-03)。
|
|
1014
|
+
for (const candidate of [record.review, record.advisoryReview]) {
|
|
1015
|
+
const classifyController = candidate?.classifyController ?? null
|
|
1016
|
+
if (classifyController === null) continue
|
|
1017
|
+
try {
|
|
1018
|
+
classifyController.abort(new Error('approval resolved'))
|
|
1019
|
+
} catch {
|
|
1020
|
+
/* ignore */
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1020
1023
|
log.info(`[approval-consult] pending ${record.key} resolved outcome=${String(record.outcome)}`)
|
|
1021
1024
|
// 审批结束:立即失效该审批下的图片引用(字节仍由 DSH 附件服务保留,不在这里删)。
|
|
1022
1025
|
try {
|
|
@@ -1271,6 +1274,25 @@ export function apply(ctx, config, runtime) {
|
|
|
1271
1274
|
}
|
|
1272
1275
|
}
|
|
1273
1276
|
|
|
1277
|
+
/* 上面的 await 会让出执行权:必须在不再 await 的临界段里重新核对审批状态与并发名额,
|
|
1278
|
+
否则多个并发请求会一起通过数量检查(安全审查 2026-11-03)。 */
|
|
1279
|
+
if (record.resolvedAt !== null) {
|
|
1280
|
+
throw httpError(409, 'approval-resolved', '这次审批已经结束了,不能继续提问。')
|
|
1281
|
+
}
|
|
1282
|
+
if (target === 'reviewer') {
|
|
1283
|
+
const reviewAfterAwait = record.review ?? record.advisoryReview ?? null
|
|
1284
|
+
if (reviewAfterAwait === null || reviewAfterAwait.options === null || reviewAfterAwait.options === undefined) {
|
|
1285
|
+
throw httpError(409, 'reviewer-context-missing', '本次审批的审查材料已失效,请重新评估后再提问。')
|
|
1286
|
+
}
|
|
1287
|
+
if (reviewAfterAwait.advisory === true && reviewAfterAwait.decision === null) {
|
|
1288
|
+
throw httpError(409, 'review-not-initialized', '审查材料已就绪,但还没有重新评估。请先点「让审查 AI 重新评估」,再继续提问。')
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
const activeJobsAfterAwait = [...record.jobs].filter((jobId) => state.jobs.get(jobId)?.status === 'streaming').length
|
|
1292
|
+
if (activeJobsAfterAwait >= cfg.maxConcurrentJobs) {
|
|
1293
|
+
throw httpError(429, 'too-many-jobs', `同一审批下最多同时有 ${cfg.maxConcurrentJobs} 个回答在生成,请等其中一个结束。`)
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1274
1296
|
const entry = {
|
|
1275
1297
|
id: `q:${nextSeq()}`,
|
|
1276
1298
|
question,
|
|
@@ -1618,37 +1640,55 @@ export function apply(ctx, config, runtime) {
|
|
|
1618
1640
|
if (review.decision !== null) {
|
|
1619
1641
|
return { review, reused: true }
|
|
1620
1642
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
const options = {
|
|
1624
|
-
...review.options,
|
|
1625
|
-
maxTokens: 1800,
|
|
1626
|
-
signal: controller.signal,
|
|
1643
|
+
if (record.resolvedAt !== null && record.resolvedAt !== undefined) {
|
|
1644
|
+
throw httpError(409, 'approval-resolved', '这次审批已经结束了,不能继续重新评估。')
|
|
1627
1645
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1646
|
+
// 同一条记录只允许一个进行中的评估:重复请求共享同一个 Promise,避免重复计费与输出混合。
|
|
1647
|
+
if (review.classifyPromise !== undefined && review.classifyPromise !== null) {
|
|
1648
|
+
await review.classifyPromise
|
|
1649
|
+
return { review, reused: true }
|
|
1650
|
+
}
|
|
1651
|
+
const task = (async () => {
|
|
1652
|
+
const controller = new AbortController()
|
|
1653
|
+
const timer = setTimeout(() => controller.abort(new Error('advisory review timeout')), 90_000)
|
|
1654
|
+
review.classifyController = controller
|
|
1655
|
+
const options = {
|
|
1656
|
+
...review.options,
|
|
1657
|
+
maxTokens: 1800,
|
|
1658
|
+
signal: controller.signal,
|
|
1640
1659
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1660
|
+
state.sideOptions.add(options)
|
|
1661
|
+
state.stats.sideCalls += 1
|
|
1662
|
+
try {
|
|
1663
|
+
for await (const chunk of ctx.llm.stream(options)) {
|
|
1664
|
+
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
|
|
1668
|
+
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)
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
review.decisionText = review.text.trim()
|
|
1674
|
+
review.decision = parseReviewDecision(review.decisionText)
|
|
1675
|
+
if (review.decision === null && review.error === null) {
|
|
1676
|
+
review.error = `审查输出无法解析:${clip(review.decisionText, 200)}`
|
|
1677
|
+
}
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
review.error = friendlySideCallError(error)
|
|
1680
|
+
noteError('advisory-classify', error)
|
|
1681
|
+
} finally {
|
|
1682
|
+
clearTimeout(timer)
|
|
1683
|
+
state.sideOptions.delete(options)
|
|
1684
|
+
review.classifyController = null
|
|
1645
1685
|
}
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1686
|
+
})()
|
|
1687
|
+
review.classifyPromise = task
|
|
1688
|
+
try {
|
|
1689
|
+
await task
|
|
1649
1690
|
} finally {
|
|
1650
|
-
|
|
1651
|
-
state.sideOptions.delete(options)
|
|
1691
|
+
review.classifyPromise = null
|
|
1652
1692
|
}
|
|
1653
1693
|
return { review, reused: false }
|
|
1654
1694
|
}
|
|
@@ -2479,20 +2519,26 @@ export function apply(ctx, config, runtime) {
|
|
|
2479
2519
|
}
|
|
2480
2520
|
const serviceLimit = Number(attachmentStore.imageLimits?.maxImageBytes)
|
|
2481
2521
|
const limit = Math.min(cfg.maxAttachmentBytes, Number.isFinite(serviceLimit) && serviceLimit > 0 ? serviceLimit : MAX_ATTACHMENT_BODY_BYTES)
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
let ref = null
|
|
2522
|
+
// 先占名额再读取/保存:超额或并发上传不会浪费宿主存储与解析资源。
|
|
2523
|
+
reserveAttachmentSlot(record.key)
|
|
2485
2524
|
try {
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2525
|
+
const data = await readBinaryBody(req, limit)
|
|
2526
|
+
if (data.length === 0) throw httpError(400, 'attachment-bad-request', '图片内容为空。')
|
|
2527
|
+
let ref = null
|
|
2528
|
+
try {
|
|
2529
|
+
ref = await attachmentStore.saveImage({
|
|
2530
|
+
data,
|
|
2531
|
+
mediaType,
|
|
2532
|
+
name: sanitizeAttachmentName(query.get('name')),
|
|
2533
|
+
})
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
throw mapAttachmentError(error)
|
|
2536
|
+
}
|
|
2537
|
+
const item = registerAttachment(ref, record.key, record.sessionId)
|
|
2538
|
+
sendJson(res, 200, { ok: true, attachment: attachmentMeta(item) })
|
|
2539
|
+
} finally {
|
|
2540
|
+
releaseAttachmentSlot(record.key)
|
|
2493
2541
|
}
|
|
2494
|
-
const item = registerAttachment(ref, record.key, record.sessionId)
|
|
2495
|
-
sendJson(res, 200, { ok: true, attachment: attachmentMeta(item) })
|
|
2496
2542
|
}),
|
|
2497
2543
|
)
|
|
2498
2544
|
addDisposer(
|
|
@@ -2994,7 +3040,7 @@ export function apply(ctx, config, runtime) {
|
|
|
2994
3040
|
})()
|
|
2995
3041
|
return {
|
|
2996
3042
|
ok: true,
|
|
2997
|
-
version:
|
|
3043
|
+
version: PLUGIN_VERSION,
|
|
2998
3044
|
uptimeMs: Math.round(process.uptime() * 1000),
|
|
2999
3045
|
shim,
|
|
3000
3046
|
config: {
|
|
@@ -3222,6 +3268,30 @@ export function apply(ctx, config, runtime) {
|
|
|
3222
3268
|
return out
|
|
3223
3269
|
}
|
|
3224
3270
|
|
|
3271
|
+
/** 该审批下进行中的上传预占数。 */
|
|
3272
|
+
function pendingAttachmentCount(consultKey) {
|
|
3273
|
+
return state.attachmentPending.get(consultKey) ?? 0
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
/**
|
|
3277
|
+
* 上传前预占名额:超额/并发上传不会先 saveImage 再被拒(安全审查 2026-11-03)。
|
|
3278
|
+
* 调用方必须在 finally 里 releaseAttachmentSlot。
|
|
3279
|
+
*/
|
|
3280
|
+
function reserveAttachmentSlot(consultKey) {
|
|
3281
|
+
const existing = attachmentsForConsult(consultKey).length
|
|
3282
|
+
const pending = pendingAttachmentCount(consultKey)
|
|
3283
|
+
if (existing + pending >= cfg.maxAttachmentsPerConsult) {
|
|
3284
|
+
throw httpError(413, 'attachment-too-many', `同一审批下最多保留 ${cfg.maxAttachmentsPerConsult} 张图片。`)
|
|
3285
|
+
}
|
|
3286
|
+
state.attachmentPending.set(consultKey, pending + 1)
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
function releaseAttachmentSlot(consultKey) {
|
|
3290
|
+
const pending = pendingAttachmentCount(consultKey)
|
|
3291
|
+
if (pending <= 1) state.attachmentPending.delete(consultKey)
|
|
3292
|
+
else state.attachmentPending.set(consultKey, pending - 1)
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3225
3295
|
/** 登记一张已存入附件服务的图片,返回对外展示的元数据。 */
|
|
3226
3296
|
function registerAttachment(ref, consultKey, sessionId) {
|
|
3227
3297
|
const existing = attachmentsForConsult(consultKey)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-approval-consult",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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",
|
|
37
|
+
"test": "node tests/unit.mjs && node tests/locale-parity.mjs && node tests/css-classes.mjs && node tests/security.mjs",
|
|
38
38
|
"check": "node --check lib/impl.js && node --check lib/client.js && node --check lib/index.js"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|