Skip to content

dsh-qa

Verified

@zseven-w/dsh-qa · v0.1.0 · MIT

QA orchestrator plugin for DeepSeek Harness: an agent explores your app and leaves evidence, then the explored path replays deterministically. Drives the browser, macOS desktop, iOS and Android drivers.

Install

dsh plugin add @zseven-w/dsh-qa

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Tags

Readme

DSH QA

像用户一样探索真实应用,留下证据,再把验证过的动作变成可重复的 QA 场景。
Agent 主导探索 • 证据支撑的断言 • 确定性回放 • 浏览器、桌面、iOS 与 Android

包名:@zseven-w/dsh-qa · 版本:0.1.0 · 开发者版本

English · 简体中文

快速开始 · 安全与限制 · 已知缺口 · 开发 · 文档入口

真实的 QA 浏览器回放报告:两个通过的步骤、最终断言与有界证据

随包示例的真实输出:两个步骤与最终断言均通过。这是未经编辑的 Markdown 报告的排版视图,不是内置仪表盘,也不代表四端覆盖。

为什么是 DSH QA

面向 DeepSeek Harness 的 QA 编排插件:Agent 像真实用户一样探索你的 App(发现的问题都带证据),随后把探索出的路径导出为确定性的 Replay 场景,在每次发版时跑。

  • Explore 模式 —— Agent 通过一组工具驱动应用(qa_session_start / qa_observe / qa_act / qa_assert / qa_evidence / qa_record_export / qa_replay_run / qa_session_stop),并记录它看到的一切。
  • Replay 模式 —— 声明式 QaScenario 文件(无损 JSON)确定性执行,每一步动作后都用新观察验证断言,产出经脱敏的 JSON / Markdown / JSONL 报告。
  • 驱动 —— @zseven-w/dsh-browser(BU,契约 v9)、@zseven-w/dsh-computer(CU,契约 v5)、@zseven-w/dsh-ios@zseven-w/dsh-android。驱动的安全语义只继承、不放宽:EXTERNAL_COMMIT_TARGET 一律拒绝,安全字段永久拒绝,审批门禁原样透传,unknown 回执必须重新观察。移动端会话必须显式指定设备 id,绝不回落到默认设备。移动端文本输入是有条件实现、不是一刀切:iOS 的 fill/type 只在实时驱动暴露了 dsh-ios 原生的、绑定到元素的 fillTarget/typeTarget 原语时才可用,缺方法或缺原生标识符时明确标为不可用,绝不回落到裸的全局输入;Android 的 type 在验证真实焦点后保持追加语义,Android 的 fill 仍是 FILL_PRIMITIVE_UNAVAILABLE

QA 只是编排四个独立驱动,它不替代驱动,也不扩大任何权限。fixture、单元测试或设备验收通过,只是那个被测范围内的证据,不代表所有应用都被支持。

快速开始

需要 Node.js 24.11.0 或更高。QA 编排的驱动并不包含在它自己包里:只有当会话请求某个平台时,它才按包名惰性加载对应驱动。只装 @zseven-w/dsh-qa 是一个驱动都没有的——qa_session_start 会直接报驱动未安装。按需把驱动装在旁边:

npm install @zseven-w/dsh-qa           # 编排器本体
npm install @zseven-w/dsh-browser      # 浏览器会话
npm install @zseven-w/dsh-computer     # macOS 桌面会话
npm install @zseven-w/dsh-ios          # iOS 真机 / 模拟器会话
npm install @zseven-w/dsh-android      # Android 真机 / 模拟器会话
驱动 包名 职责 额外前提
Browser(BU) @zseven-w/dsh-browser 浏览器观察与交互 本机已安装 Chrome / Edge / Chromium;驱动只会去发现,绝不下载浏览器
Computer(CU) @zseven-w/dsh-computer 原生桌面观察与交互 macOS;本地构建并授权的 Helper(辅助功能 + 屏幕录制)——见已知缺口
iOS @zseven-w/dsh-ios 显式设备的移动端会话 macOS + Xcode;显式设备 id
Android @zseven-w/dsh-android 显式设备的移动端会话 adb;显式设备 id

装完后用随包的浏览器示例验证,它只需要 dsh-qadsh-browser 和一个本地浏览器:

node node_modules/@zseven-w/dsh-qa/scripts/run-example.mjs

它会把随包的 web fixture 服务在一个临时 loopback 端口上,无头回放一个两步场景,并写出 JSON / Markdown / JSONL 报告。装好的环境会打印 [example] status: pass。细节见运行随包示例

如果要在 DSH 宿主里用,先安装或更新 DSH:

npm install -g @deepseek-ai/dsh@latest

安装 DSH 不会自动激活本插件。它的宿主入口声明在 cordis.patch.yml;独立的 stdio MCP 入口是 src/server.mjs,也通过 npm run mcp.mcp.json 暴露。开始会话前,先让所选宿主加载插件/服务并提供所需驱动。探索流程(观察 → 动作 → 断言 → 证据 → 导出)按 Explore playbook 走。

安全与限制

  • 不制造假绿: unknown 动作回执必须有新证据。运行结果只有 passinconclusivefail;覆盖不到不等于不存在。
  • 不扩权: 驱动的审批照常生效。安全字段与 EXTERNAL_COMMIT_TARGET 一律拒绝;移动端会话必须显式指定设备 id,没有默认设备回落。
  • 回放需要持久目标: 坐标、临时引用、有歧义的选择器都不会被提升进持久场景。没有稳定且可证明结果的动作会被排除在导出之外。
  • 视觉是 advisory: 视觉断言辅助定位问题,但不决定运行状态。模型的叙述不是观察到的事实。
  • 移动端支持是有条件的: iOS 文本输入要求实时的、绑定元素的原生原语和标识符。Android 的 type 会验证真实焦点;Android 的 fill 仍不可用。
  • 证据需要谨慎对待: 结构化报告走 fail-closed 脱敏,但截图里仍可能有隐私内容。请用合成测试数据,分享前先检查产物。登录态注入需要 owner 对确切 origin 的显式授权,见登录态

已知缺口

这是一个开发者版本。主链路——浏览器与桌面上的 Explore → 证据 → Export → Replay——每次改动都会被测试套件跑一遍;但下面这些是开放问题,诚实使用这个包的前提是知道它们。

  • Computer Helper 没有公证。 它是 adhoc 签名,没有 TeamIdentifier,也没有 stapled 票据,所以桌面这半边没有「装完即用」的体验:你需要从 dsh-computer 工作区自己构建 Helper,并自己授予辅助功能 + 屏幕录制权限。Developer ID 签名与公证都还没做。
  • closed shadow root 会让限定范围的 node-absent 断言出错。 浏览器驱动只穿透 open shadow root;closed root 的内容从不会被采集,也没有任何截断原因把它计入,于是一个 light 树装得下的 scope 会自称完整,node-absent 就可能在「容器里其实有该节点」时通过。不要在基于 closed shadow root 的 UI 上依赖「不存在」类断言。
  • 大页面上的深层目标只能是 inconclusive。 超出驱动 100 节点观察窗口后,限定范围的滚动证明最多只能到 INCONCLUSIVE_SCOPE,永远拿不到 pass。这是诚实,不是坏掉——但它意味着大页面上的深层流程今天产不出绿色门禁。
  • 视觉断言是 advisory,而且这里的实时视觉链路未经验证。 视觉按设计从不改变 Replay 的 pass/fail。通向真实宿主视觉服务(ctx.llm / attachments)的接缝在测试里只由 fake 覆盖,尚未对着真实视觉模型跑通。
  • 移动端驱动没有契约版本。 Browser 钉在契约 v9、Computer 钉在 v5,但 dsh-ios / dsh-android/driver 不导出版本;QA 靠结构化鸭子类型加载它们,所以漂移只能事后发现,而不是在加载时就拦住。

Replay 验证五类语义断言。pass 的含义是这些断言在新观察上成立——不代表应用是对的,不代表画面看着没问题,也不代表覆盖是完整的。

实现参考

会话语义、回放、观察完整性与宿主集成

会话内核

src/session/ 在驱动适配器接口之上实现 QA 循环 观察 -> 动作 -> 重新观察 -> 判定 -> 证据 -> 清理,并带这几条硬规则:

  • unknown 动作回执永远不算成功——结果只由一次新的观察决定;
  • rejected / failed 回执作为步骤失败向上传播,回执本身作为证据附上;
  • 每个会话即使失败也一定清理(driver.stop)。

src/adapters/browser.ts@zseven-w/dsh-browser(以 link:../dsh-browser 的形式声明在 devDependencies,绝不是运行时依赖,也绝不内联进包)适配到该接口,过程中不放宽任何驱动安全语义。fixtures/web/index.html 是一个自包含的 loopback fixture,复现 2026-08-25 的验收流程。

原生(Computer 驱动)fixture fixtures/native/ 按决策只留在仓库里(QA-BL-042,2026-09-05):它是由 main.swift 构建出的、已签名的 macOS app bundle,不进发布包。在工作区里用 node fixtures/native/build-fixture.mjs 构建,启动 fixtures/native/build/DshQaFixture.app,用 pkill -x dsh-qa-fixture 停止。它是演示 Computer 驱动「安全字段永久拒绝」("Secure password",fixture.securePassword)的唯一隔离目标,由 test/computer-integration.test.mjs 驱动。

回放

声明式 QaScenario 文件(无损 JSON,{ meta, target, steps[], assertions[] })由 src/replay/runner.ts 在会话内核之上确定性执行。每一步都带着动作,以及该动作之后必须在观察上成立的断言;unknown 回执永远不是通过——只有重新观察才有判定权。fail-closed 的加载器(src/replay/loader.ts)拒绝未知字段、畸形步骤、缺失字段和非无损值,且不回显值字节。报告器通过 src/redaction 里的 fail-closed v2 引擎产出脱敏后的 report.json / report.md / 只追加的 report.jsonl。参见 scenarios/examples/qa_assert / qa_replay_run

Explore → Replay

src/explore/ 把现有驱动适配器包成一个被动记录器。它在套用 fail-closed 脱敏投影之后,按顺序记录观察、动作、回执和证据引用;会话内核本身不变。临时的驱动 ref 会被替换成会话内的关联别名,绝不进入轨迹。

qa_record_export 接受一个位于当前工作区或临时目录下的 output_path,从该动作之后已稳定的新观察合成每一步的断言,写出场景文件,再通过现有的 fail-closed 加载器把写出的确切字节读回来。被拒绝/失败的动作、没有新观察的动作、以及视图始终没稳定下来的动作,都会出现在 excludedActions 里,绝不会被悄悄提升成步骤。

选择器的持久性是刻意严格的。浏览器目标要求唯一且非空的 role + 可访问名;computer 目标用持久的 Accessibility 标识符;移动端目标优先用稳定的 Android resourceId / iOS AXUniqueId。下标、坐标、观察 id、生成 id、重名、无名节点和临时 ref 一律拒绝,而不是去猜。unknown 回执还额外需要一个可观察的语义变化或 URL 变化;旧目标只是「还在」不构成证明。

Explore 方法论以两份产物交付:skills/qa-explore/SKILL.md 的人类可读副本(包含在包的 files 里),以及插件真正通过可选 skill 服务注册的 playbook——src/skill.ts 里的 QA_SKILL_CONTENT 模板字面量,经 ctx.inject(['skills'], …)qa-orchestration 之名注册。

有界 settle(异步 UI)

真实 UI 是异步的,所以动作之后立刻取一次证明观察其实是竞态:它可能错过还没渲染出来的结果,也可能抓到无关的迟到 hydration 抖动并把它当成结果。src/session/settle.ts 用有界 settle 取代这种一次性读取:持续观察,直到语义投影(排除 ref 及其他会话内标识)在 QA_SETTLE_QUIET_MS 内保持不变,整体受 QA_SETTLE_BUDGET_MS 约束;并且在动作刚发生之后,绝不从「安静」本身得出结论。

会话内核对 Explore 的证明观察和 Replay 的验证观察套用同一套策略,所以两侧不可能对同一页面看到不同的视图。始终不稳定的视图就是诚实地不可证明:导出会排除该步骤(ASSERTION_NOT_PROVABLE),回放会让该步骤失败。没有任何东西会为了让不稳定的页面通过而被放宽。断言合成优先采用动作目标上或附近的证据,当只有较远的变化可用时,会把这一弱点记进步骤意图。fill 在自己目标上的值回显会从「是否变化」的判定里屏蔽掉(那是预期内的,不是下游结果的证据),并且只要稳定后的视图显示目标带着输入的文本,导出器就会用一条针对该目标的 node-value 断言来证明这次 fill。

配置方式:new QaToolHost({ settle: ... })runScenario(..., { settle: ... }),或环境变量 DSHPLUGIN_QA_SETTLE_BUDGET_MS / DSHPLUGIN_QA_SETTLE_QUIET_MS / DSHPLUGIN_QA_SETTLE_INTERVAL_MS。完整推理、两种复现出来的真实失效模式,以及回归 fixture:docs/SETTLE.md

被截断的视图是不完整,不是空

观察受预算限制,并带 truncated 标记。超出预算的节点依然存在,所以绝不能从被截断的视图证明「不存在」node-absent 在那里 fail-closed,而不是报一个静默的假绿;任何会建立在不完整视图上的结论,都会先以 QA_ESCALATED_NODE_BUDGET 重新观察一次再下结论。在更完整的视图上依然无法证明的主张,会带上 completeness.reason: "INCONCLUSIVE_TRUNCATED"——与普通失败区分开;而已经返回的节点仍是「存在」的可靠证据。只要截断影响过某个判定,completeness 块(预算、截断状态、升级情况)就会写进 report.jsonreport.jsonl,并在 report.md 里出现一行 "view completeness"。回放执行器用同样的方式解析动作目标,导出器会把被截断的证明观察记为步骤意图里的 Weak proof: 备注。完整推理:docs/TRUNCATION.md

「不存在」要通过,必须有已验证的覆盖(QA-BL-052 -> C2,契约 v9)。 node-absent 的含义是「没有驱动可观察的语义节点」——驱动的投影会排除隐藏元素和零矩形元素——它通过的条件是:没有任何匹配,做判定的那个视图是完整的(truncated: false), coverage.verified: true:终局的「不存在」判定会在它唯一那次判定性重新观察上请求驱动的有界 closed-shadow-root 探测(绝不在 settle 轮询上做;限定范围时约 5 ms,在超大页面上做整页探测可能超预算——那样这个「不存在」就是 UNPROVEN)。探测若发现了 closed shadow root 或没有完成,结果保持 INCONCLUSIVE_TRUNCATED / COVERAGE_UNVERIFIED,并在 completeness.detailtruncationReasons 里点名 closed-shadow-root / shadow-coverage-unverified;通过的「不存在」会打印 "No driver-observable semantic node matching {predicate} was found within {scope|the whole page}; coverage verified (N nodes probed). K hidden candidates excluded."。返回了匹配节点依然照常让 node-absent 失败,INCONCLUSIVE_TRUNCATED 的语义不变。

限定范围观察(浏览器驱动契约 v9)

浏览器的钳制仍是 100 节点,而真实页面远超这个数,所以 qa_observe 接受 within_ref——一个来自调用方当前观察的不透明 ref——只观察以该元素为根的组合子树。预算、字节上限、扫描窗口和 iframe 标记都变成相对子树的:子树装得下的容器会报 truncated: false 且没有 truncationReasons,整页窗口里够不到的深层目标因此变得可达。限定范围的 node-absent 只在判定性重读的覆盖探测验证了该子树时才通过(见上)。观察的 scope 字段({ ref, rootRef, role, name, tag })回显驱动实际观察的根;rootRef(契约 v9)是每次观察新生成的 ref,即使可见性门禁把根隐藏了,它也能把下一次限定范围读取串起来。整页观察不带这些字段。未知、过期、已消费、非元素或已脱离文档的 ref 会带着驱动错误码(REF_UNKNOWN / REF_EXPIRED / TARGET_CHANGED / …)拒绝调用——绝不回落到整页,也绝不报「没找到」。computer 驱动不支持限定范围,会拒绝 within_ref

限定范围观察会流进 Replay:场景断言可以带 scope: { role, name, tag?, path? }(空名按字面保留;path 是容器在录制时的语义祖先链,最外层在前,有就记录——这是更强的回放定位符)。执行器在整页视图里按唯一谓词加上已记录的 path 来解析该容器(有歧义时报 TARGET_NOT_UNIQUE,绝不猜)。在一个被截断的整页视图里只匹配到一个,并不证明唯一性:没有 path 的非滚动证明 scope 会把预算升级一次,若视图仍被截断则以 INCONCLUSIVE_TRUNCATED 拒绝并点名该 scope;而限定范围的滚动证明步骤——或任何带已记录 path 的 scope——会以暂定方式解析。暂定解析意味着该步骤带 scopeResolution: 'provisional'reason: INCONCLUSIVE_SCOPE绝不是 pass:容器可能是错的那个(窗口外的孪生体),所以 PASS 只留给已证明的解析。被回放的滚动依然会执行,并通过驱动的身份锚点验证(验证性读取上的 anchorLastAction):锚点必须报告原始被操作元素仍连接在文档上、包含在该容器内、且在视口中,并且绑定到与被断言目标相同的节点 ref——绑定丢失或不匹配会作为步骤上的 scopeRefusal 披露出来,绝不按谓词重新选择。运行状态现在是三态:只有每个必需步骤和最终断言都被完全证明才是 pass;至少有一个结果是暂定且没有明确失败时是 inconclusive;其余是 fail。判定视图是限定范围时,completeness 块会点名该 scope,这样「在这个容器里不存在」永远不会被读成「整页不存在」——而且「不存在」只在覆盖已验证时才通过。导出时,只有当容器谓词在一个完整的、且不是该容器自身子树的录制基线观察里唯一,才会把限定范围的证明持久地记为限定范围断言;唯一性未被证明时,身份锚定的滚动证明会被显式导出为暂定(步骤意图里写明),其余所有限定范围断言一律以 SCOPE_NOT_DURABLE 排除——绝不悄悄导出成整页证明。见 docs/TRUNCATION.md

视觉断言(advisory)

qa_assert kind:"visual" 捕获当前画面,并向宿主视觉模型提一个问题。判定是 advisory:它记进报告,按 schema 排除在确定性比对之外,并且从不改变运行的三态状态(pass / inconclusive / fail)。

有两条规则来自对 deepseek-v4-flash-vision-exp 的实弹使用:

  • verdictconfidence;绝不要把 reasoning 当成观察到的事实引用。 在一次真实运行里,模型对「衬线体 WIKIPEDIA 字标是否存在」正确回答了 yes(置信 1.00),然后叙述说「带着拼图地球 logo」——而那个 logo 根本不在页面上。推理文本作为定位问题的上下文保留,但每条 advisory 记录都在 report.json / report.jsonlqa_assert 结果里带 reasoningTrust: "unverified-model-narration"report.md 则把它放在 ## Advisory (model-generated; never affects pass/fail) 标题下、渲染成一个标注了「模型叙述」的引用块。推理与完整决策:docs/REDACTION_SPEC.md 第 7.3 节。
  • 捕获永远绑定到一次新观察。 两个驱动都会拒绝绑定到陈旧观察的捕获(浏览器驱动:"the semantic observation expired; observe again before visual capture"),因为 Set-of-Mark 标注和像素必须描述同一个视图。与其放宽这条规则或复用旧帧,captureLatestVisual 会在每次捕获之前、为两个驱动都取一次新的稳定观察——于是 qa_assert 视觉和 qa_evidence visual:true 可以直接跟在 qa_act 或彼此之后,中间不需要手动 qa_observe。调用方显式钉住的捕获(浏览器 visual_fingerprint、computer observationId)绝不会被悄悄刷新:钉住会被尊重,陈旧的钉住则按设计被驱动拒绝。

宿主运行时

本包在 dependencies/peerDependencies 里声明零个宿主包。下列运行时服务由 DSH 宿主自己提供。npm install @zseven-w/dsh-qa 必须一个 @deepseek-ai/* 包都不带进来——这是一条打包不变式,由 scripts/smoke-pack.mjs 针对真实打包安装强制执行。

{
  "dshHostRuntime": {
    "services": ["tools", "attachments", "llm", "approval"],
    "typing": "structural"
  }
}

可选的 skill 服务只通过 ctx.inject(['skills'], cb) 注册,并用 fiber.dispose() 拆除。

运行随包示例

包的载荷里带一个自包含的浏览器示例:scenarios/examples/fixture-web.json 驱动 fixtures/web/ 页面。装好 @zseven-w/dsh-qa@zseven-w/dsh-browser 之后,直接从安装好的副本运行——不需要本仓库工作树里的任何文件:

node node_modules/@zseven-w/dsh-qa/scripts/run-example.mjs

(或者在安装目录内执行 npm run example。)

run-example.mjs 会把 fixtures/web/ 服务在一个临时 loopback 端口上(绝不用写死的端口),把场景的 target.launch 重新绑定到那个 origin,通过浏览器驱动无头回放,并写出 report.json / report.md / report.jsonl。装好的环境会打印:

[example] status: pass
[example] report: <dir>/report.json

退出码为 0 当且仅当 status === "pass";用 --output-dir <dir> 指定报告目录(默认 ./dsh-qa-example-report)。在 DSH 安装里浏览器驱动由宿主提供;在独立安装里,请把 @zseven-w/dsh-browser 装在旁边。scripts/smoke-pack.mjs 会从打包出的 tarball 跑这条完全相同的命令并断言 status: "pass"

开发

开发使用 pnpm 10.34.5 和兄弟目录布局:四个驱动仓库是 link: 开发依赖,所以请把它们克隆在本仓库旁边(dsh-browserdsh-computerdsh-iosdsh-android),并满足各自的构建前提。CI 不克隆它们——它用 .github/scripts/fetch-drivers.sh 从已发布的 tarball 还原出同样的目录,这也是你在本地针对已发布驱动字节复现 CI 失败的方式。

pnpm install
npm run build        # 同时构建 MCP server 与插件入口
npm run typecheck
npm test             # 全量;前提见下
npm run test:ci      # 没有已授权 Helper 的机器能跑的全部内容
npm run smoke:bundle # 从无 node_modules 的副本做真实 stdio 握手
npm run smoke:pack   # prepack 门禁 -> 打包 -> 全新安装 -> 握手 + 示例

npm test 是验收命令,而且它不跳过:它需要 macOS、一个已构建并已授权的 DSH Computer Helper(辅助功能 + 屏幕录制),以及一个已安装的浏览器。test/computer-integration.test.mjs 在缺少授权时会大声失败而不是空转通过,这正是 test:ci 按文件名排除这一个文件、而不是在测试内部加守卫的原因——托管 CI 的绿说的是「除那个点名的套件外全部通过」,绝不是「那个套件通过了」。不要发布生成的报告、登录态、设备标识符或签名材料。

文档入口

docs/ 和 Explore playbook 都随包发布,所以这些引用在安装好的副本里也能读到,不只是在仓库里。

许可

MIT