# AGENTS.md 给在这个仓库里干活的 agent(和以后的自己)看的。用户向的说明在 [README](./README.md)。 ## 这是什么 DeepSeek Harness(DSH)插件:识别工作区里的 Minecraft 模组工程,并在会话标题栏提供一个 IDEA 风格的运行控件(选中目标 → 启动 / 停止 / 看日志)。 界面不是全部。面板与工具共用同一份运行状态与同一把锁:**人**看工程状态、起停、读日志; **模型**识别工程、启动任务、拿日志、拿结论。同一工程同时只有一个实例,两边不会互相踩。 两者的分工来自运行目标的两类生命周期: - `daemon`(`runClient` / `runServer`):一直跑到人关窗口,"就绪"是个有意义的状态; - `oneshot`(`runData` / `runGameTestServer` / 自建 `runGametest`):跑完即结束, 结束时的 `verdict` 才是它的产出。模型的"改 → 跑 → 验"闭环靠这一类。 窗口画面是模型判断不了的部分,交给人;能断言的(构建、数据生成、gametest 结果)交给模型。 - 宿主侧:ESM、无 TypeScript、JSDoc 注解;Node ≥ 20。 - 客户端侧:React 组件用 `h()` 手写(不用 JSX),由 esbuild 打成 CJS,再包一层 `window.__ModuleLoader__.load({ id, factory })`。 - 设计文档:[策划](./doc/策划.md)(架构、规则、里程碑)、[DSH 插件机制调研](./doc/调研-DSH插件机制.md)。 ## 目录 ``` src/index.js 宿主入口:inject + apply,挂 HTTP 路由与工具 src/tools.js 六个模型工具(mc_projects / mc_project_detail / mc_launch / mc_launch_logs / mc_launch_list / mc_launch_stop) src/lib/ api.js /mcdev/api 的方法实现(scan / detail / state / launch.*) detect.js 工程画像:loader、模块、run 目标、证据与警告 scanner.js 工作区 BFS(深度 3、剪枝、上限) gradle.js build.gradle / settings.gradle / gradle.properties 的解析 metadata.js mods.toml / fabric.mod.json / plugin.yml / archive.yml java.js JDK 探测与版本匹配(读 /release,不起进程) deps.js 声明依赖:dependencies 块、libs.versions.toml、元数据 depends jdks.js 每个工程的 JDK 选择(优先级、jdk.json 持久化、要不要注入) verify.js 目标分类(daemon / oneshot)与一次性任务的裁决(verdict) launches.js 启动服务:spawn、就绪判定、日志环形缓冲、停止、状态对账 state.js ~/.dsh/mc-dev-starter 下的状态与日志(运行记录按 profile 分目录) argv.js gradlew 命令行构造(含 Windows 引号规则) idea.js IntelliJ `.idea` 工程模型(模块清单 / facet / 工程 JDK / 运行配置) xml.js 极简 XML 解析(零依赖,只服务 .idea) text.js / toml.js 小工具 src/client/ index.js 客户端入口:胶囊 slot、两个标签类型、better-sidebar 可选注册 Capsule.js 标题栏胶囊(状态点 + 目标名 + 启停) Flyout.js 浮层(单工程特化 / 多工程三段结构) DetailTab.js 工程面板(版本、环境、模块、证据、警告) LogTab.js 日志标签(历史、跟随、归因) state.js / selection.js / format.js / icons.js / styles.js / runtime.js lib/client.js 构建产物(.gitignore 忽略;靠 package.json 的 files 打进 npm 包,运行时直接加载) test/ 156 个测试 + fixtures(真实工程形态的假仓库;fixtures-multi 是多模块聚合工程) doc/ 策划与调研 ``` ## 命令 ```powershell node --test # 全部测试(默认发现 test/*.test.js;Node 20 不认 glob,别写 "test/*.test.js") npm test # 同上,先重建客户端再跑 node scripts/build-client.mjs # 构建客户端(改 src/client 后必须跑) node scripts/build-client.mjs --watch node scripts/check-pack.mjs # 发布前校验 npm 包内容(缺 lib/client.js、多带 test/ 都会失败) node scripts/scan-catalog.mjs # 扫真实工程,输出识别结果表 ``` CI 在 `.github/workflows/ci.yml`:ubuntu / windows / macOS × Node 20/22/24 跑构建与测试, 另有一个 pack 任务跑 `check:pack`。`prepack` 会在打包前自动构建,客户端漏构建发不出去; `test/bundle.test.js` 按宿主加载协议真跑一遍产物,客户端挂掉不用等用户装完才发现。 改完客户端**必须**重新构建 `lib/client.js` 并跑测试;样式改动还要过 `test/styles.test.js` (颜色/字号必须引用 `--dsw-*` 令牌,不许写字面色值)。 ## 开发期安装 ```powershell node /@deepseek-ai/dsh/lib/bin.js plugin --profile m0test add link:C:/Projects/minecraft-dev-starter node /@deepseek-ai/dsh/lib/bin.js --profile m0test --port 3499 --no-open ``` `link:` 才能改完即生效;`file:` 会被 pnpm 拷贝成副本,编辑仓库不影响已装副本。 ## 硬坑(都踩过,别再踩) | 现象 | 原因 | 做法 | |---|---|---| | 客户端整包不加载 | 没有 `__ModuleLoader__.load` 包装、或 factory 里用了 ESM | esbuild 打 CJS,banner/footer 由 `scripts/build-client.mjs` 生成 | | `ctx.get('x')` 拿到 undefined | cordis 要求服务先声明在 `inject` | 需要什么写进 `inject`;可选服务用 `ctx.inject([...], cb)` 或 `ctx.get(name, false)` | | 注册 slot 报 "not declared" | 父条目还没声明该 slot | 用 `ctx.slots.inject(name, …)`,或注册失败后重试 | | 启动后日志 0 字节 | Windows 上 `detached: true` 让子进程丢掉我们的管道(`DETACHED_PROCESS` 与 `CREATE_NO_WINDOW` 冲突) | Windows 关闭 detached,保留 `windowsHide`;POSIX 才 detached(要进程组) | | 启动时多一个控制台窗口 | 同上:没有可继承的隐藏控制台,java 自己开一个 | 同上,关闭 detached 后消失 | | 中文日志乱码 | `.bat` 输出按 GBK 编码 | `decodeLine`:先按 UTF-8 解,失败再按 GBK | | 工具报 "value is not lossless JSON" | 返回值里带了 `undefined` | 只放存在的字段;测试里用 `assertLossless()` | | 改 profile 后启动失败 | `package.json` 写入了 BOM | 用无 BOM 的 UTF-8 写;改完必须重启 DSH | | 仓库的 `node_modules` 神秘消失 | 在某个 profile 里把 `link:` 依赖换成 `file:`/npm 包时,pnpm 解链会**删掉被链接目录的 `node_modules`**(实测:`plugin add link:` → 在 → `plugin add file:tgz` → 没了) | 别在同一 profile 里来回切换安装方式;tarball/registry 验证用独立 profile;发现消失就 `npm install --ignore-scripts` 重装 | | 侧栏标签打不开 | `openTab` 的展开是异步的 | 打开后等一帧再断言 `isExpanded()` | | 发版后装不到新版本 | pnpm 缓存了 registry 元数据(`%LOCALAPPDATA%\pnpm-cache\metadata-v1.3\registry.npmjs.org\.json`),`dsh plugin add` 会报 "latest is 0.1.2"、甚至"只有 4 个版本" | 删掉那个 `.json` 再装;同时确认 `.npmrc` 里的代理是通的 | | 一次性任务被当成守护进程 | 用"等就绪"的语义管 `runData` / `runGameTestServer`:`BUILD SUCCESSFUL` 是**终止**信号却被当成**就绪**信号,而且跑完拿不到"过没过" | 目标带 `mode`(daemon / oneshot),oneshot 生下来就是 running、结束即算 `verdict`;见 `src/lib/verify.js` | | `runs { configureEach { } }` 变成假目标 | `runs` 是 NamedDomainObjectContainer,容器 API 的块也被当成 run 配置,产出 `runConfigureEach`(启动报 Task not found) | 容器 API 名单 + 只收顶层块 + 认 `create("name")`;见 `parseRuns` | | 测试挂了却判通过 | 部分 GameTest 运行器失败时进程仍退 0 | 裁决先看失败特征行、再看退出码;`readVerdict` 里顺序不能调 | | `libs.versions.toml` 解析成一整条字符串 | `parseToml` 不支持内联表,`{ module = "a:b", version = "1" }` 原样返回 | 已补 `parseInlineTable`;回归见 `test/toml.test.js` 与 `test/deps.test.js` | | 读 `.idea` 时抛 `Cannot read properties of undefined` | 只有 `modules.xml`、没有 `misc.xml` 的工程:`componentOf(undefined)` 直接取 `.name` | `rootOf` / `componentOf` 容忍 undefined;回归见 `test/idea.test.js` | | worktree 里的 `node_modules` 一删,主仓库的也没了 | 用 junction 指向主仓库的 `node_modules`,`git worktree remove` 递归删除时会跟着目标走 | 别在 worktree 里 junction 主仓库依赖;要跑测试就复制或用独立 `npm ci`,删之前先确认链接类型 | | 本地全绿、CI 全红 | 测试夹具(`.idea`、图片等)被 `.gitignore` 忽略:本地有文件,干净 checkout 没有 | 夹具目录要在 `.gitignore` 里显式放行;发版前用 `git clone` 到临时目录跑一遍 | 其他约定: - 用户能看到的每个结论都必须带证据(loader 从哪个插件来、目标从哪个 run 块来); `unknown` 是合法答案,不许猜成确定值。 - 推测出来的目标要标 `guessed: true`,UI 显示「未验证」,不许隐藏。 - 证据源有主次:**构建脚本是主、`.idea` 是辅**。`.idea` 只补缺(模块清单、facet 平台、工程 JDK、 运行配置),绝不推翻构建脚本已经判定的 loader 与目标;补出来的目标标 `source: 'idea-run-config'`, 不是 `guessed`——它在 IDE 里真的被跑过。 - 多模块工程里子模块各自成工程,任务路径必须带 Gradle 模块前缀、在聚合根执行; 「在哪个目录跑 gradlew」这件事由识别结果决定,不允许调用方指定。 - 不做隐含的 `clean`;构建目录不碰。 - 一个工程同时只允许一个实例;跨工程可以并行。 - 状态文件读写要 tmp + rename,进程探活失败记 `unknown`,不伪造结束原因。 ## 验证记录 历史里程碑的验证结论。原始长文已并入这里,`doc/` 只保留设计与调研。 ### M0 加载协议与槽位(2026-10-30,通过) 客户端包能在真实运行时里加载、激活、注册标题栏 slot 与右侧栏标签,并能驱动 `openTab` 展开侧栏。 产出四个后续一直沿用的结论:bundle 必须 `__ModuleLoader__.load` 包装;CSS 以 `