dsh-escalation-review 0.2.0
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.
Potentially problematic release.
This version of dsh-escalation-review might be problematic. Click here for more details.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/NOTICE.md +17 -0
- package/README.md +186 -0
- package/README.zh.md +162 -0
- package/config.json +72 -0
- package/cordis.patch.yml +11 -0
- package/icon.svg +14 -0
- package/lib/breaker.js +76 -0
- package/lib/client.js +1672 -0
- package/lib/config-schema.js +162 -0
- package/lib/config.js +210 -0
- package/lib/decisions.js +67 -0
- package/lib/dsh-packages.js +128 -0
- package/lib/facts.js +277 -0
- package/lib/index.js +543 -0
- package/lib/policy.js +766 -0
- package/lib/probes.js +337 -0
- package/lib/projection.js +114 -0
- package/lib/reviewer.js +265 -0
- package/lib/selftest-loader.js +41 -0
- package/lib/session-key.js +31 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +63 -0
package/lib/reviewer.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reviewer.js —— LLM 评审流水线:取流式判定、解析严格 JSON、失败即拒。
|
|
3
|
+
*
|
|
4
|
+
* 与 policy.js 的分工:policy 决定"给模型看什么、怎么判读文本"(纯函数、可单测),
|
|
5
|
+
* reviewer 决定"怎么拿到模型的回答"(依赖宿主 ctx 的 llm 服务 + 懒加载 BlockAssembler)。
|
|
6
|
+
* 失败路径必须与"政策拒绝"可区分:文案不同、info.reason 可回传模型。
|
|
7
|
+
*
|
|
8
|
+
* 重试语义(2026-09-27 起,按用户定的数字):
|
|
9
|
+
* · 最多 3 次(`MAX_REVIEW_ATTEMPTS`)
|
|
10
|
+
* · **单次尝试上限** `attemptTimeoutMs`(默认 30s)—— 有了它,超时不再等于预算花光
|
|
11
|
+
* · **总预算** `timeoutMs`(默认 100s)—— 3×30 + 2×5 = 100,正好被兜住
|
|
12
|
+
* · **重试间隔** `retryDelayMs`(默认 5s,带 0.9–1.1 抖动)
|
|
13
|
+
* · **可重试**:单次超时、解析/形状错误、限流/过载/5xx/408/429/连接类
|
|
14
|
+
* · **不重试**:调用方取消、总预算耗尽、输入预算超限、缺 provider/model
|
|
15
|
+
* · 证据(事实/探针/快照)只算一次;每次尝试只重发请求
|
|
16
|
+
*/
|
|
17
|
+
import { importDshPackage , lastResolutionRoot} from './dsh-packages.js'
|
|
18
|
+
|
|
19
|
+
/** 最近一次成功解析 DSH 内部包所用的解析根(导入器可能失败,这里绝不抛)。 */
|
|
20
|
+
function safeRoot() {
|
|
21
|
+
try {
|
|
22
|
+
return lastResolutionRoot() ?? 'unknown'
|
|
23
|
+
} catch {
|
|
24
|
+
return 'unknown'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
import { buildPolicy, buildSnapshot, readDecision, renderSnapshot } from './policy.js'
|
|
28
|
+
import { collectLocalFacts } from './facts.js'
|
|
29
|
+
import { runSelectedProbes } from './probes.js'
|
|
30
|
+
|
|
31
|
+
/** 评审自身最多尝试几次(照 Codex `MAX_REVIEW_ATTEMPTS = 3`)。 */
|
|
32
|
+
export const MAX_REVIEW_ATTEMPTS = 3
|
|
33
|
+
/** 旧退避基数(已被 `retryDelayMs` 取代,保留导出以免打断既有引用)。 */
|
|
34
|
+
export const RETRY_BASE_MS = 200
|
|
35
|
+
|
|
36
|
+
/** 懒解析的 BlockAssembler 缓存:undefined=未解析;null=不可用(走兜底);function=可用。 */
|
|
37
|
+
let assemblerCache
|
|
38
|
+
/** 诊断注入(`__diagnostics.setAssembler`):非 null 时优先使用。 */
|
|
39
|
+
let assemblerOverride = null
|
|
40
|
+
|
|
41
|
+
/** 诊断注入 assembler(index.js 的 `__diagnostics.setAssembler` 转发到这里)。 */
|
|
42
|
+
export function setAssemblerOverride(value) {
|
|
43
|
+
assemblerOverride = value
|
|
44
|
+
assemblerCache = undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `ready` 日志里的 assemblerSource:'lazy' | 'test-override'。 */
|
|
48
|
+
export function assemblerSourceName() {
|
|
49
|
+
return assemblerOverride === null ? 'lazy' : 'test-override'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 懒解析 BlockAssembler。
|
|
54
|
+
* 关键:**绝不在注册监听器时 await 这个 import** —— 在 Electron 的 ESM 解析下它可能既不
|
|
55
|
+
* resolve 也不 reject(会让 effect 永久挂住,监听器永远注册不上,且看不到任何日志)。
|
|
56
|
+
* 这里带 3 秒超时,拿不到就返回 null,由 readDecision 走已验证的兜底提取路径。
|
|
57
|
+
*/
|
|
58
|
+
export async function resolveAssembler(log) {
|
|
59
|
+
if (assemblerOverride !== null) return assemblerOverride
|
|
60
|
+
if (assemblerCache !== undefined) return assemblerCache
|
|
61
|
+
// 用 importDshPackage:GUI 安装是 link: 依赖,模块 URL 落在 workspace,
|
|
62
|
+
// 裸 import('@deepseek-ai/dsh-llm') 会 ERR_MODULE_NOT_FOUND(已实测)。
|
|
63
|
+
try {
|
|
64
|
+
const llm = await Promise.race([
|
|
65
|
+
importDshPackage('@deepseek-ai/dsh-llm'),
|
|
66
|
+
new Promise((_resolve, reject) => setTimeout(() => reject(new Error('import timed out after 3000ms')), 3000)),
|
|
67
|
+
])
|
|
68
|
+
if (typeof llm?.BlockAssembler === 'function') {
|
|
69
|
+
assemblerCache = llm.BlockAssembler
|
|
70
|
+
log?.write('assembler-resolved', { specifier: '@deepseek-ai/dsh-llm', root: safeRoot() })
|
|
71
|
+
return assemblerCache
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
log?.write('assembler-import-failed', {
|
|
75
|
+
specifier: '@deepseek-ai/dsh-llm',
|
|
76
|
+
message: String(error?.message ?? error),
|
|
77
|
+
attempts: error?.attempts,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
assemblerCache = null
|
|
81
|
+
log?.write('assembler-unavailable', { note: '改用从流里提取 JSON 的兜底路径(严格协议不变)' })
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 这次失败值得重试吗(照 Codex `should_retry_guardian_review`)。
|
|
87
|
+
* 不可重试:超时/取消、缺 provider/model、输入预算类。
|
|
88
|
+
* 可重试:解析与形状错误、限流/过载/5xx/408/429、连接类。
|
|
89
|
+
*/
|
|
90
|
+
export function isRecoverableReviewError(error) {
|
|
91
|
+
const message = String(error?.message ?? error)
|
|
92
|
+
if (/timed out|timeout|aborted|abort|cancell?ed|signal/i.test(message)) return false
|
|
93
|
+
if (/no (provider|model) available/i.test(message)) return false
|
|
94
|
+
if (/input budget|context window|context length|budget exceeded/i.test(message)) return false
|
|
95
|
+
if (/does not match|unexpected members|must be one JSON object|could not find a JSON|invalid (risk|authorization|outcome)|must carry a reason|must not carry a reason|critical risk|authorization at least/i.test(message)) {
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
if (/rate ?limit|overload|too many|429|408|5\d\d|stream|connection|ECONNRESET|socket|fetch failed/i.test(message)) return true
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 取 shell 执行器(只给 `probeRunner: 'shell'` 用)。
|
|
108
|
+
* 顺序:agent 的 ctx(agent 本来就在跑命令,那里一定有)→ 插件 ctx。逐段 try(cordis 里访问未声明服务属性会抛)。
|
|
109
|
+
*/
|
|
110
|
+
function safeGetShell(exec) {
|
|
111
|
+
const scopes = [exec?.agent?.ctx ?? exec?.agent?.context, exec?.agent?.session?.ctx]
|
|
112
|
+
for (const scope of scopes) {
|
|
113
|
+
if (scope === undefined || scope === null) continue
|
|
114
|
+
try {
|
|
115
|
+
if (scope.shell !== undefined && scope.shell !== null) return scope.shell
|
|
116
|
+
} catch {
|
|
117
|
+
/* 未声明时属性访问会抛 */
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
if (typeof scope.get === 'function') {
|
|
121
|
+
const viaGet = scope.get('shell')
|
|
122
|
+
if (viaGet !== undefined && viaGet !== null) return viaGet
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
/* get 也可能抛 */
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function review(ctx, exec, cfg, log, effective, extra = {}) {
|
|
132
|
+
const BlockAssembler = await resolveAssembler(log)
|
|
133
|
+
// 只读核实:先收事实(进程内、无命令),再按动作选探针并在额度内执行(固定 argv,无 shell)
|
|
134
|
+
const facts = collectLocalFacts(exec)
|
|
135
|
+
// 探针是**进程内**实现(不 spawn、不依赖宿主内部 spec 契约)
|
|
136
|
+
// 探针通道由配置决定:inproc(默认)或 shell(沙箱内只读命令);拿不到 ctx.shell 时探针自己回退并记账
|
|
137
|
+
const probeRunner = cfg.probeRunner === 'shell' ? 'shell' : 'inproc'
|
|
138
|
+
const probeOutcome = await runSelectedProbes(exec, facts, {
|
|
139
|
+
runner: probeRunner,
|
|
140
|
+
shell: safeGetShell(exec),
|
|
141
|
+
workdir: exec?.agent?.session?.header?.cwd,
|
|
142
|
+
})
|
|
143
|
+
if (probeOutcome.probes.length > 0 || probeOutcome.budgetExceeded) {
|
|
144
|
+
log?.write('probed', {
|
|
145
|
+
ids: probeOutcome.probes.map((probe) => probe.id),
|
|
146
|
+
budgetExceeded: probeOutcome.budgetExceeded === true,
|
|
147
|
+
ms: probeOutcome.probes.reduce((total, probe) => total + (probe.ms ?? 0), 0),
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
const snapshot = buildSnapshot(ctx, exec, effective, cfg, {
|
|
151
|
+
facts,
|
|
152
|
+
...probeOutcome,
|
|
153
|
+
previousReviews: Array.isArray(extra.previousReviews) ? extra.previousReviews : [],
|
|
154
|
+
currentTurn: extra.turnId,
|
|
155
|
+
})
|
|
156
|
+
// 把"这次评审手里有什么"记下来:否则探针/事实/历史评审是否生效只能靠猜
|
|
157
|
+
log?.write('review-context', {
|
|
158
|
+
facts: snapshot.localFacts?.facts?.length ?? 0,
|
|
159
|
+
probes: (snapshot.probes ?? []).length,
|
|
160
|
+
probeNotes: snapshot.probeNotes ?? [],
|
|
161
|
+
previousReviews: (snapshot.previousReviews ?? []).length,
|
|
162
|
+
previousReviewsWithheld: snapshot.previousReviewsWithheld ?? 0,
|
|
163
|
+
evidenceComplete: snapshot.evidence?.authorizationComplete === true,
|
|
164
|
+
// 每条探针的结果:只记数量的话,"通道可达但命令失败"看不出来(2026-09-27 实测 ms=1 起疑)
|
|
165
|
+
probeResults: (snapshot.probes ?? []).map((probe) => ({
|
|
166
|
+
id: probe.id,
|
|
167
|
+
ok: probe.ok === true,
|
|
168
|
+
ms: probe.ms,
|
|
169
|
+
...(probe.timedOut === true ? { timedOut: true } : {}),
|
|
170
|
+
...(probe.skipped === undefined ? {} : { skipped: probe.skipped }),
|
|
171
|
+
...(probe.runner === undefined ? {} : { runner: probe.runner }),
|
|
172
|
+
...(probe.sandboxMode === undefined ? {} : { sandboxMode: probe.sandboxMode }),
|
|
173
|
+
// 记下探针"看到了什么"(有界),否则只能看到 ok/失败
|
|
174
|
+
...(probe.output === undefined ? {} : { output: String(probe.output).slice(0, 80) }),
|
|
175
|
+
...(probe.error === undefined ? {} : { error: String(probe.error).slice(0, 120) }),
|
|
176
|
+
})),
|
|
177
|
+
})
|
|
178
|
+
const provider = typeof cfg.provider === 'string' && cfg.provider.length > 0 ? cfg.provider : snapshot.provider
|
|
179
|
+
const model = typeof cfg.model === 'string' && cfg.model.length > 0 ? cfg.model : snapshot.model
|
|
180
|
+
if (typeof provider !== 'string' || provider.length === 0) throw new Error('no provider available for the reviewer')
|
|
181
|
+
if (typeof model !== 'string' || model.length === 0) throw new Error('no model available for the reviewer')
|
|
182
|
+
const effort = typeof cfg.reasoningEffort === 'string' ? cfg.reasoningEffort.trim() : ''
|
|
183
|
+
// 每次尝试都重新渲染(内容一致,成本可忽略),system 只算一次
|
|
184
|
+
const system = buildPolicy(cfg)
|
|
185
|
+
const userText = renderSnapshot(snapshot)
|
|
186
|
+
// 三档超时(2026-09-27 定:单次 30s / 总 100s / 间隔 5s → 3×30 + 2×5 = 100s,正好被总预算兜住):
|
|
187
|
+
// · attemptTimeoutMs:**单次尝试**上限。有了它,"本次超时"不再等于"预算花光" → 超时**可以重试**
|
|
188
|
+
// · timeoutMs:**整个评审**的总预算。调用方(agent)在审批路径里等着,总等待必须有界
|
|
189
|
+
// · retryDelayMs:两次尝试之间的等待
|
|
190
|
+
const totalTimeoutMs = typeof cfg.timeoutMs === 'number' && cfg.timeoutMs > 0 ? cfg.timeoutMs : 100_000
|
|
191
|
+
const attemptTimeoutMs = Math.max(
|
|
192
|
+
1_000,
|
|
193
|
+
Math.min(typeof cfg.attemptTimeoutMs === 'number' && cfg.attemptTimeoutMs > 0 ? cfg.attemptTimeoutMs : 30_000, totalTimeoutMs),
|
|
194
|
+
)
|
|
195
|
+
const retryDelayMs = typeof cfg.retryDelayMs === 'number' && cfg.retryDelayMs >= 0 ? cfg.retryDelayMs : 5_000
|
|
196
|
+
const deadline = Date.now() + totalTimeoutMs
|
|
197
|
+
/** 剩余预算低于这个值就不再开始新尝试(避免白打一次注定失败的上游请求)。可配置。 */
|
|
198
|
+
const minAttemptMs = Math.max(
|
|
199
|
+
0,
|
|
200
|
+
Math.min(typeof cfg.minAttemptMs === 'number' && cfg.minAttemptMs >= 0 ? cfg.minAttemptMs : 2_000, attemptTimeoutMs),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
/** 单次尝试:超时取"单次上限"与"剩余总预算"里更小的那个。 */
|
|
204
|
+
const attemptOnce = async () => {
|
|
205
|
+
const remaining = Math.max(1, deadline - Date.now())
|
|
206
|
+
const timeout = AbortSignal.timeout(Math.max(1, Math.min(attemptTimeoutMs, remaining)))
|
|
207
|
+
const signal = exec?.signal ? AbortSignal.any([exec.signal, timeout]) : timeout
|
|
208
|
+
const stream = ctx.llm.stream({
|
|
209
|
+
provider,
|
|
210
|
+
model,
|
|
211
|
+
system,
|
|
212
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: userText }] }],
|
|
213
|
+
temperature: 0,
|
|
214
|
+
// 思考强度:留空=交给 provider/模型默认(与 Composer 的"思考强度"同义)
|
|
215
|
+
...(effort === '' ? {} : { reasoningEffort: effort }),
|
|
216
|
+
signal,
|
|
217
|
+
})
|
|
218
|
+
return readDecision(stream, BlockAssembler ?? undefined)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let attempt = 1
|
|
222
|
+
for (;;) {
|
|
223
|
+
try {
|
|
224
|
+
const decision = await attemptOnce()
|
|
225
|
+
if (attempt > 1) log?.write('review-succeeded-after-retry', { attempt })
|
|
226
|
+
// 本次判定的授权版本(照 Codex GuardianAuthorizationVersion):调用方按它给历史评审打标
|
|
227
|
+
return { ...decision, authorizationVersion: snapshot.authorizationVersion }
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const message = String(error?.message ?? error)
|
|
230
|
+
const remaining = deadline - Date.now()
|
|
231
|
+
// 区分三种"中止":① 调用方取消(不重试)② 总预算耗尽(不重试)③ 本次尝试超时(**可以**重试)
|
|
232
|
+
const callerAborted = exec?.signal?.aborted === true
|
|
233
|
+
const totalExpired = Date.now() >= deadline || remaining <= 0
|
|
234
|
+
const attemptTimedOut = !callerAborted && !totalExpired && /timed out|timeout|aborted|abort/i.test(message)
|
|
235
|
+
const retryable = isRecoverableReviewError(error) || attemptTimedOut
|
|
236
|
+
if (attempt >= MAX_REVIEW_ATTEMPTS || !retryable || totalExpired) throw error
|
|
237
|
+
const jitter = 0.9 + Math.random() * 0.2
|
|
238
|
+
const delay = Math.min(Math.round(retryDelayMs * jitter), Math.max(0, remaining - 1))
|
|
239
|
+
// 预算不足就不再开始"注定失败"的尝试(否则会白打一次上游请求)
|
|
240
|
+
if (remaining - delay < minAttemptMs) {
|
|
241
|
+
log?.write('review-retry-skipped', {
|
|
242
|
+
attempt,
|
|
243
|
+
reason: 'insufficient-budget',
|
|
244
|
+
remainingMs: remaining,
|
|
245
|
+
plannedDelayMs: delay,
|
|
246
|
+
minAttemptMs,
|
|
247
|
+
message: message.slice(0, 200),
|
|
248
|
+
})
|
|
249
|
+
throw error
|
|
250
|
+
}
|
|
251
|
+
log?.write('review-retry', {
|
|
252
|
+
attempt,
|
|
253
|
+
reason: attemptTimedOut ? 'attempt-timeout' : 'error',
|
|
254
|
+
delayMs: delay,
|
|
255
|
+
remainingMs: remaining,
|
|
256
|
+
attemptTimeoutMs,
|
|
257
|
+
totalTimeoutMs,
|
|
258
|
+
message: message.slice(0, 200),
|
|
259
|
+
})
|
|
260
|
+
await sleep(delay)
|
|
261
|
+
if (Date.now() >= deadline) throw error
|
|
262
|
+
attempt += 1
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftest-loader.js —— 可选的策略自测用例装载器。
|
|
3
|
+
*
|
|
4
|
+
* 自测用例是可选的附加模块,默认未安装:装载器按下面的顺序查找,取不到就抛错,
|
|
5
|
+
* 调用方记录 selftest-unavailable 后继续工作 —— 自测是诊断功能,缺它不影响评审。
|
|
6
|
+
*
|
|
7
|
+
* 查找顺序:
|
|
8
|
+
* 1) 环境变量 DSH_ER_SELFTEST_MODULE(绝对路径)
|
|
9
|
+
* 2) 配置项 selftestModule(绝对路径)
|
|
10
|
+
* 3) 包内 ./selftest.js(相对本文件)
|
|
11
|
+
*/
|
|
12
|
+
import { pathToFileURL } from 'node:url'
|
|
13
|
+
|
|
14
|
+
function toSpecifier(value) {
|
|
15
|
+
if (typeof value !== 'string' || value.trim().length === 0) return undefined
|
|
16
|
+
const text = value.trim()
|
|
17
|
+
// 只有真正的 URL scheme 当 URL;Windows 盘符(D:/…)必须走 pathToFileURL,否则 import 会报 ERR_UNSUPPORTED_ESM_URL_SCHEME\n if (/^(file|node|data|https?):/i.test(text)) return text
|
|
18
|
+
return pathToFileURL(text).href
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function loadSelfTest(cfg) {
|
|
22
|
+
const candidates = [
|
|
23
|
+
toSpecifier(process.env.DSH_ER_SELFTEST_MODULE),
|
|
24
|
+
toSpecifier(cfg ? cfg.selftestModule : undefined),
|
|
25
|
+
new URL('./selftest.js', import.meta.url).href,
|
|
26
|
+
].filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
27
|
+
|
|
28
|
+
const attempts = []
|
|
29
|
+
for (const specifier of candidates) {
|
|
30
|
+
try {
|
|
31
|
+
const mod = await import(specifier)
|
|
32
|
+
if (typeof mod?.runSelfTest === 'function') return mod
|
|
33
|
+
attempts.push(specifier + ': no runSelfTest export')
|
|
34
|
+
} catch (error) {
|
|
35
|
+
attempts.push(specifier + ': ' + String(error?.code ?? error?.message ?? error))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const failure = new Error('optional self-test module is not installed')
|
|
39
|
+
failure.attempts = attempts
|
|
40
|
+
throw failure
|
|
41
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-key.js —— 会话与 turn 的身份(从 index.js 抽出来,便于单测与复用)
|
|
3
|
+
*
|
|
4
|
+
* turn 的近似:**用户消息条数**。用户每说一句话就算一个新 turn,
|
|
5
|
+
* 等价于 Codex 的 clear_turn / user_message_revision —— 熔断计数与"历史评审版本"都靠它。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 会话标识:优先 session.id,退回 header.sessionId,最后用固定串兜底(绝不抛)。 */
|
|
9
|
+
export function sessionKeyOf(exec) {
|
|
10
|
+
try {
|
|
11
|
+
return String(exec?.agent?.session?.id ?? exec?.agent?.session?.header?.sessionId ?? 'session')
|
|
12
|
+
} catch {
|
|
13
|
+
return 'session'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** turn 标识:用户消息条数(解析失败一律算 0,绝不抛)。 */
|
|
18
|
+
export function turnIdOf(exec) {
|
|
19
|
+
try {
|
|
20
|
+
const events = exec?.agent?.session?.snapshotEvents?.() ?? []
|
|
21
|
+
let count = 0
|
|
22
|
+
if (Array.isArray(events)) {
|
|
23
|
+
for (const event of events) {
|
|
24
|
+
if (event?.type === 'user/message') count += 1
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return String(count)
|
|
28
|
+
} catch {
|
|
29
|
+
return '0'
|
|
30
|
+
}
|
|
31
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-escalation-review",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Escalation-only LLM reviewer: reviews sandbox escapes only, keeps the sandbox, fails closed.",
|
|
5
|
+
"icon": "./icon.svg",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": {
|
|
11
|
+
"default": "./lib/client.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./locale/*.json": "./locale/*.json",
|
|
15
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
16
|
+
"./config.json": "./config.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib/**/*.js",
|
|
20
|
+
"locale/*.json",
|
|
21
|
+
"icon.svg",
|
|
22
|
+
"config.json",
|
|
23
|
+
"cordis.patch.yml",
|
|
24
|
+
"README.md",
|
|
25
|
+
"README.zh.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"NOTICE.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
33
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.2 <0.3.0"
|
|
34
|
+
},
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
},
|
|
39
|
+
"client": {
|
|
40
|
+
"platform": "web",
|
|
41
|
+
"inject": [
|
|
42
|
+
"@deepseek-ai/dsh-client-locale",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/trentswd/dsh-escalation-review.git"
|
|
51
|
+
},
|
|
52
|
+
"author": "trentswd",
|
|
53
|
+
"keywords": [
|
|
54
|
+
"dsh",
|
|
55
|
+
"dsh-plugin",
|
|
56
|
+
"deepseek-harness",
|
|
57
|
+
"escalation",
|
|
58
|
+
"review",
|
|
59
|
+
"approval",
|
|
60
|
+
"sandbox",
|
|
61
|
+
"codex-style"
|
|
62
|
+
]
|
|
63
|
+
}
|