# eadp-dsh-plugin **Repository Path**: eadp/eadp-dsh-plugin ## Basic Information - **Project Name**: eadp-dsh-plugin - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-11-03 - **Last Updated**: 2026-11-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # @eadp/dsh-plugin EADP 的 DSH 插件包(一个包、多个 plugin entry)。五个模块全部落地: `app-context`(W07)、`factory-tools`(W08)、`approval-bridge` / `telemetry` / `remote-api`(W09)。 - 任务:`EADP-QC-2026-0309`(W07)、`EADP-QC-2026-0310`(W08)、`EADP-QC-2026-0311`(W09) - 运行基准:DSH `0.2.1-rc.5`(源码 tag `dsh-v0.2.1-rc.5`) - 契约:`edap-docs/protocol/AppContextManifest-v1.0.md`(协议 v1.1) - **使用与维护手册:技术规范库 `TS-2026-0015`**(本地副本 `edap-docs/techspec/`)。 动代码前先读它——本 README 讲「这个包长什么样」,TS-2026-0015 讲「装机、排障、加模块、跟协议升级」。 --- ## 1. 它解决什么 会话启动时,把「这是哪个应用、按哪套骨架、走到哪一步、能碰什么、不许做什么」 **放进工作区、也放进模型上下文**,不再依赖人写 prompt。 两条通道,各有各的活: | 通道 | 落点 | 角色 | 开关 | | --- | --- | --- | --- | | A 文件 | `.eadp/manifest.json` 等五件 | 审计、复现、提示词失效时的降级路径 | 恒开 | | B 提示词 | `ctx.systemPrompt.context()` | 主通道(P1 实测通过,每轮求值) | `EADP_CONTEXT_PROMPT=off` 可关 | 通道 B 注册失败不影响通道 A:`registerAppContextPrompt` 抛错会被降级成 「只走文件」,而不是把整个插件拖下水。 --- ## 2. 目录结构 ``` eadp-dsh-plugin/ ├── cordis.patch.yml # bundle 层:插入入口插件 + 摆开关 ├── src/ │ ├── index.ts # 入口:按配置选择性挂载子模块,不承载业务 │ ├── config.ts # 配置解析(config > env > 默认值),问题只记不抛 │ ├── app-context/ # ★ 已实现(W07) │ │ ├── index.ts # 编排:拉取 → 落盘 → 注入;状态机与降级 │ │ ├── manifest.ts # 拉取:重试分类 + 本地缓存 + 协议校验 │ │ ├── writer.ts # 落盘:原子写 + 容器校验 + 幂等 │ │ ├── render.ts # 渲染:CONSTRAINTS.md / STAGE_SPEC.md / 提示词摘要 │ │ └── prompt.ts # 通道 B:context 注册 + 首次装配拦截 │ ├── factory-tools/ # ★ 已实现(W08) │ │ ├── index.ts # 注册四个工具 │ │ ├── generate.ts # eadp_generate:校验 generator schema → 调生成器 │ │ ├── guard/ # eadp_guard:规则集 + 检查器注册表 │ │ ├── build.ts # eadp_build:固定命令跑构建 │ │ ├── artifact.ts # eadp_artifact:产物归档 + 上报 │ │ ├── context.ts # 读 manifest、派生标识只读守卫、路径容器校验 │ │ ├── envelope.ts # 统一信封 + 留痕(.eadp/traces) │ │ ├── params.ts # 参数校验(JSON Schema 实用子集) │ │ └── process.ts # 子进程执行器(超时 + 杀进程树 + 输出截尾) │ ├── approval-bridge/ # ★ 已实现(W09):审批桥(fail-closed) │ │ └── index.ts # approval/request → 编排层 → 决策回注 │ ├── remote-api/ # ★ 已实现(W09):内网端点**出站客户端** │ │ ├── client.ts # baseUrl / x-eadp-token / 超时 / 错误分类 │ │ └── endpoints.ts # 四个内网路径 + 裁决解析 │ ├── telemetry/ # ★ 已实现(W09):事件批量上报 + 断线重连 │ └── shared/ # http / fsx / log / time │ └── types/ # manifest 协议镜像 + DSH 接缝面的最小结构类型 └── test/ # node --test:95 个用例 ``` **五个模块默认全开。** 未配置 `orchestrator.baseUrl` 时,审批桥与上报自动不挂载 (不会发出无意义请求),远程 API 只记一行自检日志。 --- ## 3. 落盘契约 | 文件 | 内容 | 谁读 | | --- | --- | --- | | `.eadp/manifest.json` | 完整 run manifest(原样快照,不加字段) | 执行器 / 审计 / 复现 | | `.eadp/CONSTRAINTS.md` | 身份、DB 硬约束、分层、骨架与守卫、基线、模型 | Agent(人也能读) | | `.eadp/STAGE_SPEC.md` | 本阶段输入 / 必读 / 产出 / 挂载矩阵 / 下一步 | Agent | | `.eadp/context/{runId}/stage-{n}.json` | 版本化快照(协议 §11 定的路径) | 复现 | | `.eadp/context/last-fetch.json` | 本次拉取元信息(来源/尝试次数/时间/哈希) | 运维排查 | | `.eadp/cache/context-*.json` | 成功一次的缓存,降级时用 | 插件自己 | | `.eadp/CONTEXT_ERROR.md` | **仅失败时**出现的诊断文件,成功即删 | 人 | | `.eadp/traces/{date}/{tool}-{id}.json` | 每次工具调用的入参摘要与结果(W08) | 审计 | | `.eadp/artifacts/{runId}/stage-{n}.json` | 产物清单(sha256 / size / 上报状态) | 归档 / 复现 | 写文件的三个硬要求: 1. **容器校验**:落盘走 node:fs,不经过沙箱(沙箱管的是模型发起的工具调用), 所以每次写都先 `resolveWithin(workspace)`,被污染的 `runId` 写不出工作区。 2. **原子写**:临时文件 + rename,读方不会看到半截 JSON。 3. **幂等**:`contentHash` 一致且四个契约文件都在,就整体跳过(只重写流水账), 不让「文件变了」变成噪声。 --- ## 4. 配置 YAML(`cordis.patch.yml`)里只放**不随进程变的东西**;随进程变的走环境变量。 | 配置项 | 环境变量 | 默认 | 说明 | | --- | --- | --- | --- | | `enabled` | `EADP_PLUGIN_ENABLED` | `true` | 总开关 | | `logLevel` | `EADP_LOG_LEVEL` | `info` | silent / error / warn / info / debug | | `modules.*` | — | 全部 `true` | 模块开关(构建期决定) | | `workspace.root` | `EADP_WORKSPACE_ROOT` | `process.cwd()` | 必须是绝对路径 | | `orchestrator.baseUrl` | `EADP_ORCHESTRATOR_URL` | 空 | 如 `http://127.0.0.1:18096` | | `orchestrator.token` | `EADP_ORCHESTRATOR_TOKEN` | 空 | 作为 `x-eadp-token` 发送 | | `orchestrator.timeoutMs` | `EADP_ORCHESTRATOR_TIMEOUT_MS` | `5000` | 单次请求 | | `orchestrator.retries` | `EADP_ORCHESTRATOR_RETRIES` | `3` | **总尝试次数**(含首次) | | `orchestrator.retryBaseDelayMs` | `EADP_ORCHESTRATOR_RETRY_DELAY_MS` | `400` | 第 n 次重试前等 `base × 2^(n-1)` | | `context.appId` | `EADP_APP_ID` | 空 | 四元组之一 | | `context.runId` | `EADP_RUN_ID` | 空 | 四元组之一 | | `context.stageNo` | `EADP_STAGE_NO` | `0` | 1–8;`0` = 没配 | | `context.versionId` | `EADP_VERSION_ID` | 空 | 可省,但复现时必须显式给 | | `prompt.enabled` | `EADP_CONTEXT_PROMPT` | `true` | 通道 B 开关 | | `approval.timeoutMs` | `EADP_APPROVAL_TIMEOUT_MS` | `300000` | 单次审批最多阻塞多久(超时交回上游 fail closed) | | `telemetry.batchSize` | `EADP_TELEMETRY_BATCH_SIZE` | `20` | 满批立即上报 | | `telemetry.flushIntervalMs` | `EADP_TELEMETRY_FLUSH_MS` | `2000` | 定时冲刷(决定「5 秒内到达」) | | `telemetry.maxQueue` | `EADP_TELEMETRY_MAX_QUEUE` | `1000` | 积压上限,溢出丢最旧并计数 | 取值优先级 **config > env > 默认**(空串、`0`、缺键都算「没配」)。 `prompt.enabled` 故意没写进 `cordis.patch.yml`——写死 true 会让环境变量永远盖不住它。 四元组不齐时**不发请求**:完全没配按 debug 静默跳过,配了一半按 warn 报出来。 --- ## 5. 失败语义 | 情况 | 结果 | 模型看到什么 | | --- | --- | --- | | 远程成功 | `phase=ready` | 摘要(正常) | | 远程失败、缓存可用 | `phase=degraded` | 摘要首行带【注意】+ 失败原因 + 缓存时间 | | 远程失败、无缓存 | `phase=failed` | 空串(不注入),并写 `CONTEXT_ERROR.md` | | 落盘失败(权限/磁盘) | `phase=degraded` | 摘要仍注入(通道 B 不受影响) | | 四元组不全 | 不装配 | 无 | 重试只覆盖**可能自愈**的失败:网络、超时、5xx、429。 4xx 与「响应体不是 JSON」直接判死——重试它们只是把同一个错误说三遍。 **插件在启动阶段绝不抛错**:抛错会让整个插件被卸载,连带把文件通道也关掉。 --- ## 6. 工厂工具(W08) 四个工具都在 `ctx.tools` 注册,名字统一 `eadp_` 前缀。返回**同一个信封**: ```json { "ok": true, "tool": "eadp_guard", "status": "violations", "message": "…", "data": { "…": "…", "summary": ["给模型看的三五行"] }, "trace": { "id": "…", "persistedPath": ".eadp/traces/2026-11-01/eadp_guard-….json" } } ``` `status` 取值:`ok` / `violations`(守卫不通过,不是工具故障)/ `rejected`(参数越界)/ `not_ready`(前置未就绪,如骨架包没声明 generator 契约或契约 JSON 坏了)/ `failed`(执行失败)。 | 工具 | 参数 | 做什么 | 边界 | | --- | --- | --- | --- | | `eadp_generate` | `kind`(必填)、`name`、`params`、`dryRun` | 按骨架包 `generator/schema.json` 校验参数,再调 `generator.entry` 生成 | 派生标识只读;schema / entry 缺失或 schema 不是合法 JSON → `not_ready` | | `eadp_guard` | `files`、`rules`、`includeWarnings`、`resetRounds` | 跑规则集(内置 G1–G13,骨架包 `guard/rules.json` 可覆盖),返回 `{ruleId, level, file, line, message}`;带 3 轮回环 | G7(controller 胖)与 G13(分层 / 跨模块 import)已实现;第 3 轮仍 ERROR → `status=escalate` + 写 `review.md` | | `eadp_build` | `target`(fe/be/all)、`timeoutMs` | `npm run build`(在 `fe/`)/ `mvn -B -DskipTests package`(在 `be/`) | **命令固定**,不接受自定义命令;超时杀进程树 | | `eadp_artifact` | `paths`(必填)、`note` | 算 sha256 + size,写 `.eadp/artifacts/{runId}/stage-{n}.json`,尝试上报 | 阶段号**只认 manifest**,不接受自选;路径必须在工作区内 | 两条契约(W08 冻结,W10 已按此实现): ```jsonc // 骨架包 generator/schema.json { "schemaVersion": "1.0", "generators": { "crud-page": { "type": "object", "properties": { }, "required": [] } } } // 骨架包 manifest.json 里的入口声明 { "generator": { "entry": { "command": "node", "args": ["generator/generate.mjs"] } } } // stdin 收 { kind, name, params, app, scaffold, workspace };stdout 回 { "files": ["…"] } ``` 已知边界:构建与生成走 `node:child_process`,**不经过 DSH 的 sandbox seam**(那条路管的是模型工具调用)。 蓝图把容器级隔离放在二期,本期靠「固定命令 + 工作区 cwd + 超时 + 输出截尾」兜底。 --- ## 7. 审批桥、事件上报与内网客户端(W09) ### 7.1 审批桥 `approval/request` 是 waterfall:返回 outcome 即认领,`next()` 交给下一个应答者。 桥做的事只有一件——把请求转成编排层的业务审批,等人工决策,再把结果回注。 | 情况 | 返回 | 为什么 | | --- | --- | --- | | 编排层放行(allow / approve / approved…) | `allowed-once` | 唯一算放行的值 | | 编排层拒绝(deny / reject…) | `rejected` | **不回退 next**:拒绝不能被下一个应答者翻案 | | 没配 `baseUrl` / 网络失败 / 404 / 5xx / 超时 | `next()` | 桥没接上 = 没有审批通道,由 DSH fail closed | | 响应认不出 | `next()` | 把 `{"decision":"maybe"}` 当放行等于开天窗 | | `req.signal` 已断 | `cancelled` | 不准吞掉取消 | 默认超时 5 分钟(`approval.timeoutMs`)。P3 提示过:长等待可能先被 tool-call 超时策略打断, 真接生产前要用真实审批实测一次,别把「桥超时」和「上游超时」混为一谈。 ### 7.2 事件上报 只报 `session/event` 的**已提交记录**,不报流式增量。记录形状对齐 DSH 的 `SessionTelemetryRecord`: ```json { "channel": "ledger", "time": 1762000000000, "severity": "error", "attributes": { "session_id": "...", "event_type": "tool/result", "event_seq": 42 }, "body": { "…": "已压缩:字符串 2000 字、数组 50 项、深度 5 层、整体 4KB 封顶" } } ``` - **不拖慢会话**:`emit` 只入队,HTTP 在定时器里跑; - **5 秒内到达**:默认 2 秒冲刷,`turn/end` 额外立即冲刷(实测最旧记录 1.3s 送达); - **断线能续**:失败批次回到队首,退避 1s→2s→…→30s,恢复后把 `dropped` 一起报上去, 接收方据此知道中间有缺口,而不是以为事件连续。 ### 7.3 内网客户端(remote-api) `baseUrl` / `x-eadp-token` / 超时 / 错误分类只有一份实现(`src/remote-api/client.ts`), 审批桥、上报、产物归档都用它。 **端点归属八类 URL 第 7 类「服务间 PC」(`os` / `/sapi/eadp/ai`)**,口径见 TS-2026-0011 §2.1: 调用方是 DSH 进程(服务间)而不是浏览器,`us`(`/api/eadp/ai`)是 PC 前台走登录态; `sapi` 段与门户鉴权分离,正好对应 `x-eadp-token`。 | Method | Path | 用途 | | --- | --- | --- | | GET | `/sapi/eadp/ai/context/manifest` | 下发 manifest(当前走 bs 通道,迁移目标) | | POST | `/sapi/eadp/ai/approval/request` | 工具审批转业务审批 | | POST | `/sapi/eadp/ai/events/report` | 会话事件批量上报 | | POST | `/sapi/eadp/ai/artifacts/report` | 产物归档上报 | 这四个端点已在 `weaver-eadp` 实现(EADP-QC-2026-0336):controller 落 `controller/os/**`, 统一 `x-eadp-token` 校验(平台侧配置项 `eadp.internal.token`,**必须与插件的 `EADP_ORCHESTRATOR_TOKEN` 一致**)。审批是**同步阻塞**的:等不到裁决或超时一律按 `deny` 返回,只有明确 `allow` 才放行。端点不可用时插件侧仍按 fail closed 处理 (审批回 `next()`、上报退避重试、产物记 `pending`)。 **本期只做出站,不做入站端点**(ADR-2026-0006):`@Remote` 要 Typert 代码生成、headless 没有 `webServer`、一期编排层是主动驱动方。模块启用时只做配置自检(地址合法性与 token 是否配置), 不探活、不发请求。 ## 8. 依赖边界(升级 DSH 时要看的那一页) 本包**不 import `@deepseek-ai/*`**,只依赖文档化的接缝面: - `ctx.systemPrompt.context({name, order, text, interpolate})` - `system-prompt/assemble`(waterfall,`prepend`) - `agent/created`(`{agent, source}`) - `ctx.tools.register(ToolDefinition)`(原始 JSON Schema 形态,参数校验自己负责) - `approval/request`(waterfall,`(req, next) => Promise`) - `session/event`(`(session, event)` 的已提交记录) - `ctx.plugin()` / `ctx.effect()` 签名按 rc.5 源码抄在 `src/types/dsh.ts`(约 170 行,含源码位置)。 DSH 升级时按该文件的三条核对清单逐项验证即可,业务代码不用动。 --- ## 9. 开发与验证 ```bash npm install # 只装 typescript + @types/node npm run build # tsc → dist/ npm test # node --test,95 个用例(单元 + 真 HTTP 集成) ``` 测试覆盖:URL 拼装、信封兼容、seed 拒绝、重试与退避、缓存降级、落盘四文件与幂等、 runId 污染、渲染字段与 emoji 红线、配置优先级;工厂工具侧另覆盖参数校验子集、 守卫各规则命中与行号、规则集覆盖、generate 的 schema/entry 缺失与真实执行、 build 的命令与 cwd、artifact 的哈希/归档/上报三态与路径越界; W09 侧覆盖内网客户端(token 头 / 超时 / 非 JSON / 404)、审批桥五态与「慢 300ms 也要等」、 事件记录压缩与严重级别、批次冲刷、失败重试与 `dropped` 计数。 ### 装进 profile ```bash dsh plugin --profile headless add F:/AI/DSHCoding/eadp-dsh-plugin # 需要同时出现在 package.json 的 dsh.profile.bundles 里,否则它的 cordis.patch.yml 不生效 ``` ### 端到端复现 ```bash cd <应用工作区> export EADP_APP_ID=... EADP_RUN_ID=... EADP_STAGE_NO=... export EADP_ORCHESTRATOR_URL=http://127.0.0.1:18096 export EADP_ORCHESTRATOR_TOKEN=... # 内网接口用 # 工厂工具 dsh --profile headless "用 eadp_guard 检查 be/migrations/V2__bad.sql,只跑 G1/G3/G4,报告命中的规则与行号" # 审批桥 + 上报(编排层需实现 /sapi/eadp/ai/approval/request 与 /sapi/eadp/ai/events/report) dsh --profile headless "把文本写到工作区外;被拦就按机制请求升级,最后报告审批等了多久" ``` --- ## 10. 已知限制 1. **一个进程一个应用**(方案 A)。上下文状态是进程级的,不做 per-session 分域。 2. `agent/created` 会触发重新装配,但**装配期间已发出的请求**拿不到新上下文—— 切换 run 应当换进程。 3. 首次装配有 8 秒等待上限;超时后先让模型拿到「暂无上下文」,下一轮再补。 4. `eadp_generate` 的真实执行依赖骨架包的 `generator/schema.json` + `generator.entry`; `eadp-web-basic@0.2.0`(EADP-QC-2026-0312 / W10)已提供,`crud-page` 一次产出 18 个文件。 5. 守卫的 G7(controller 业务逻辑)与 G13(分层依赖)需要 AST/import 图,由 W11 承接; 它们会出现在 `unsupported` 里,**不代表已通过**。 6. **内网端点只有出站**:端点已由平台实现(`/sapi/eadp/ai/**`,EADP-QC-2026-0336), 但平台不可达时插件仍按 fail closed 退化:审批回 `next()`、上报退避重试(最长 30s)、 产物记 `pending`——都不影响会话本身。 7. 事件上报是**至少一次**语义:接收方要按 `(session_id, event_seq)` 去重;进程崩溃会丢掉队列里未发送的部分。 8. 审批「长等待是否先被 tool-call 超时打断」尚未在真实人工审批里实测(POC 只验证了机制)。