首页 > 教程 > DeepSeek Harness 教程 > DeepSeek Harness 防御性编程模式
DeepSeek Harness 教程

DeepSeek Harness 防御性编程模式

第 33 / 35 节 2026-11-07

为什么要防御性编程

有一类 bug“平时测不出来、一上线就出事”。dsh 官方文档把它们归纳成“来之不易的缺陷类别规则”,每条都来自真实发布或差点发布的缺陷。本章讲五个最重要的模式:结果正交上报、dispose 完全停稳、凭据擦除、链接安全删除、回调异常隔离——共同目标是不让一个简单的边界情况把整个 Agent 搞挂。

模式一:正交结果独立上报

一次运行可以同时具有多种性质:进程可能已超时,却仍捕获终止信号以退出码 0 结束。timedOut、signal、exitCode 是三个独立事实,必须各自上报,绝不能把一个标志嵌套在另一个的分支里,否则提前终止会被误判为正常成功:

export interface RunResult {
  timedOut: boolean               // 独立事实 1:是否超时
  signal: NodeJS.Signals | null   // 独立事实 2:是否被信号终止
  exitCode: number | null         // 独立事实 3:退出码
  stdout: string
  stderr: string
}

let timedOut = false
const timer = setTimeout(() => {
  timedOut = true
  child.kill('SIGTERM')
}, timeoutMs)

child.on('close', (code, signal) => {
  clearTimeout(timer)
  // 三个字段各自独立返回:timedOut=true 且 exitCode=0 是合法组合
  resolve({ timedOut, signal, exitCode: code, stdout, stderr })
})

模式二:dispose 必须达到完全停稳

清理流程若只发出终止信号就返回,而不等工作真正停止,就会留下孤儿进程。正确做法是异步等待子进程退出,并在终止前先移除监听器,让迟到的完成事件保持静默:

export async function disposeQuiescent(child: ChildProcess): Promise<void> {
  child.removeAllListeners()       // 1. 先移除监听器
  child.kill('SIGTERM')            // 2. 请求停止
  // 3. 等待真正退出;超时仍不退则升级 SIGKILL,Promise.race 防止被永远挂起
  const forceKill = new Promise<void>((resolve) => {
    setTimeout(() => { child.kill('SIGKILL'); resolve() }, 5_000)
  })
  await Promise.race([once(child, 'exit').then(() => undefined), forceKill])
}

模式三:凭据擦除

启动命令必须使用清理过的环境变量,凡名称匹配 *KEY*、*SECRET*、*TOKEN*、*PASSWORD* 的项一律移除,否则 harness 凭证可能通过命令输出、env 或 spill 文件泄漏。临时文件与 spill 文件应放在权限 0700 的私有目录中,用随机文件名并以 'wx'、0o600 独占方式打开:

const SENSITIVE_NAME = /.*(?:KEY|SECRET|TOKEN|PASSWORD).*/i

export function scrubEnv(env: NodeJS.ProcessEnv): Record<string, string> {
  const clean: Record<string, string> = {}
  for (const [key, value] of Object.entries(env)) {
    if (SENSITIVE_NAME.test(key)) continue   // 敏感键直接跳过
    if (value !== undefined) clean[key] = value
  }
  return clean
}
// 用法:spawn(argv[0], argv.slice(1), { env: scrubEnv(process.env), ... })

模式四:符号链接用 unlink 删除

可能是符号链接或 Windows junction 的路径,先用 lstatSync().isSymbolicLink() 判断,再用 unlinkSync 删除——unlink 只删链接本身并拒绝真实目录,绝不跟随链接进入目标;Windows 上对 junction 调 rmSync(link) 会抛 ERR_FS_EISDIR。只有确认是真实目录后,才使用带 recursive 的 rmSync。

模式五:分发器中隔离回调异常

用户提供的监听器抛异常时,不得让它所在的 promise 被 reject,也不得饿死排在后面的监听器。用 try/catch 包裹分发循环并记录日志——一个行为不当的订阅者绝不能破坏核心生命周期:

export function dispatch(listeners: ReadonlyArray<() => void>): void {
  for (const listener of listeners) {
    try {
      listener()
    } catch (err) {
      console.error('[dispatch] a listener failed:', err)   // 记录并继续
    }
  }
}

dsh 内部随处可见此模式:session/event 的观察者失败会被记录并隔离,而不会让已提交的 append 失败。

更多模式

另有两条值得记住:公共约定两侧都要遵守——同一结果的多种表示应在通过公共 API 返回前规范化;异步状态不是同步状态——不要把 agent/status 或 whenIdle() 当作某次 followup() 的结果,拥有一次运行的调用方必须显式定义区间。

©️版权声明:
本网站(https://aigc.izzi.cn)刊载的所有内容,包括文字、图片、音频、视频等均在网上搜集。
访问者可将本网站提供的内容或服务用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯本网站及相关权利人的合法权利。除此以外,将本网站任何内容或服务用于其他用途时,须征得本网站及相关权利人的书面许可,并支付报酬。
本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站,予以删除。

继续学习其他教程