# MC 运行面板 · DSH 插件策划 > 版本 v0.3 · 2026-10-30 · 状态:**待审核** > v0.2 → v0.3:胶囊简化(去掉日志按钮与"编辑配置")、日志并入右侧栏并带历史切换、多模块按模块分组、并发定为"每工程一个"、客户端构建链定案。 > 审阅入口:第 16 节「已定 / 待定」。机制依据见 `doc/调研-DSH插件机制.md`。 --- ## 1. 定位 ### 1.1 一句话 在 DSH 会话标题栏放一个像 IDEA 运行配置那样的控件:自动认出当前工作区里的 MC 工程和它能跑的目标(Client / Server / Data …),点一下就在后台起客户端,人自己去游戏里验收,AI 继续在对话里改代码。 ### 1.2 为什么不是"再给 AI 加几个工具" AI 做 MC 模组开发到不了全自动:纹理、手感、配方平衡、崩溃复现这些必须人进游戏看。真正卡流程的地方不是"AI 不会跑 gradlew",而是**每次验收都要人离开对话、开终端、敲命令、等构建、再回来看日志**。 把这些动作变成标题栏上的一个按钮,人和 AI 就各自干各自的事: ``` 人:改完 → 点 ▶ → 进游戏看效果 → 回来提意见 AI:读代码、改代码、看日志 → 继续 ``` AI 侧的工具保留,但它是辅助:让人不用切窗口,也让 AI 能在没人盯的时候自己起一次做验证。 ### 1.3 已确认的形态决策 | 项 | 决定 | 影响 | |---|---|---| | 入口位置 | DSH 会话标题栏的「运行」胶囊 + 点开的浮层 | 需要写 client 插件(React + slots) | | 识别范围 | 当前会话的工作区 | 不需要维护跨工作区项目列表 | | AI 工具 | 保留,作为次要能力 | 工具是同一套服务的薄封装 | | 多模块 | 浮层里按模块分组,单选一个目标 | 任务路径按 `:模块:任务` 解析 | | 并发 | 每个工程最多一个实例,跨工程可并存 | 状态机与互斥按工程维度做 | | 日志 | 复用会话右侧栏,一个标签页内切换历史 | 不占标题栏,不塞浮层 | | 配置编辑 | 不做 | 少一个入口,也少一类误操作 | --- ## 2. 现状与缺口 本机 desktop profile 已装 `minecraft-dev@0.10.1`,提供 `mc_scaffold` / `mc_gradle` / `mc_codex` 与 8 个技能。`mc_gradle` 能跑 `runClient`,但: - 它是**一次前台工具调用**:输出在调用内收集,600s 超时杀进程树,回合结束才算完。人没法在游戏里待着的同时让 AI 继续干活。 - 它只服务模型,界面上没有任何入口——人想跑一次客户端,还是得自己开终端。 - 工程信息不沉淀:每次都要现场读 `build.gradle` 才知道这是 Fabric 还是 NeoForge。 缺的三块:**工程画像**(这个工程能跑什么)、**实例托管**(跑起来之后谁在管)、**界面入口**(人怎么点)。v0.2 把界面入口放到第一优先级。 --- ## 3. 目标与边界 ### 3.1 目标 | 编号 | 目标 | 判定标准 | |---|---|---| | G1 | 界面入口可用 | 会话标题栏出现运行胶囊;点开能选目标并启动;全程不产生对话回合 | | G2 | 识别准确 | `C:\Projects` 17 个真实工程判定全对;无特征工程报 `unknown` 而非乱猜;结论带证据 | | G3 | 托管启动可靠 | 启动不阻塞界面;可在面板里停止并杀净进程树;DSH 重启后仍能查询/停止 | | G4 | 失败可诊断 | 崩溃后面板直接给出结论(Gradle 依赖失败 / JDK 不匹配 / 端口占用 / Mixin 报错)与日志定位 | | G5 | AI 可协作 | AI 能通过工具启动、读日志、停止,结果与面板同源 | ### 3.2 非目标(v1) - 下载安装 Minecraft / Mod / 整合包;账号登录。 - 图形调试器。**debug 按钮不做**——见 §16 D4 的折中方案。 - 替代 IDE;热重载、断点。 - 跨工作区项目收藏(本轮只跟会话 cwd)。 - macOS / Linux 一等支持(代码跨平台,验收只看 Windows)。 --- ## 4. 界面设计 三块:标题栏胶囊(点)、浮层(选)、右侧栏日志标签(看)。 ### 4.1 胶囊:只留三件事 ``` 会话标题栏 ┌──────────────────────────────────────────────────────────┐ │ 会话标题 [● Client ▾] [▶] [会话日志] [▤] │ └──────────────────────────────────────────────────────────┘ ↑ 状态点+目标名 ↑ 起/停 ``` - 只有状态点、目标名、起停按钮。**没有日志按钮,没有齿轮**——日志去右侧栏。 - 运行中 ▶ 变 ⏹,停止也在这里,不再另开入口。 - 多模块时目标名带模块前缀(`fabric · Client`);宽度不够只显示目标名,模块名在浮层里看。 - 没有工程时显示 `未检测到 MC 工程`,仍可点开看原因。 - 状态点:灰=空闲、黄=构建/启动中、绿=运行中、红=上次异常退出。 ### 4.2 浮层:单选,不显示多实例状态 ``` ┌────────────────────────────────────────┐ │ rationality │ │ NeoForge 1.21.1 · JDK 21 · Gradle 9.5 │ │ ────────────────────────────────────── │ │ fabric │ │ ◉ Client │ │ ○ Server │ │ forge │ │ ○ Client │ │ ────────────────────────────────────── │ │ ⓘ 2 条提示:configuration-cache 可能… │ └────────────────────────────────────────┘ ``` - **单选项**:选中哪个,胶囊的 ▶ 就起哪个。同一时刻只有一个目标被选中。 - 分组标题只在多模块时出现;单模块直接平铺 `Client / Server / Data`。 - 运行中列表置灰(先停再换目标),避免"点着点着起了两个"。 - 没有「编辑配置…」,也没有「重新构建后运行」。要 clean 就让用户自己先跑构建——不做隐式重构建,避免点一下等十分钟。 - 提示区只在有风险时出现。 ### 4.3 日志:右侧栏的一个标签页 ``` ┌ 运行日志 ─────────────────────────────────────────────┐ │ [Client · 20:31 运行中 ▾] ← 历史切换,默认最新一条 │ │ ───────────────────────────────────────────────────── │ │ [BUILD] > Task :compileJava ✓ │ │ [BUILD] > Task :runClient │ │ [MC] [Render thread/INFO]: Backend library: LWJGL │ │ [WARN] Mixin apply failed: ... │ └───────────────────────────────────────────────────────┘ ``` - 注册成右侧栏的一个标签类型(标题"运行日志"),**不用浮层,也不占标题栏**。 - 启动时自动打开并切到这次运行(配置 `autoOpenLogTab`,默认开)。 - 顶部历史切换:按时间倒序列出这个工程的运行记录,默认最新;每条显示目标、开始时间、时长/状态。日志文件按时间落在 `$DSH_HOME/mc-dev-starter/logs/.log`。 - 正文等宽、跟随尾部、可暂停滚动;`[BUILD]` / `[MC]` / `[WARN]` 按行判定着色;崩溃时顶部压一条归因。 ### 4.4 交互约定 - 点击不产生对话回合,不写会话历史。 - **一个工程同时只有一个实例**;换目标必须先停,换工程可以并存。 - 关浮层、关侧栏都不停进程。 - 切换会话:胶囊跟随新会话 cwd 重新识别;旧工程的实例继续跑,它的日志标签仍可从历史里打开。 - 识别失败时,浮层直接给证据(例如"只找到 build.gradle,但没有识别到任何 loader 声明")。 --- ## 5. 架构 界面在浏览器里,进程在宿主里,两者靠 HTTP 路由连: ``` ┌─ DSH 客户端(Web / Electron 渲染进程)─────────────┐ │ client.js(React,cordis 插件) │ │ 标题栏 slot → 胶囊 + 浮层 │ │ 右侧栏 tab → 运行日志(含历史切换) │ └───────────────────────┬───────────────────────────┘ │ fetch('/mcdev/api/') ┌───────────────────────┴───────────────────────────┐ │ DSH 宿主进程 │ │ host: ctx.webServer.register({ kind:'prefix', │ │ path:'/mcdev/api', handler }) │ │ ├─ ProjectScanner → 工程画像(按 cwd + mtime 缓存)│ │ ├─ LaunchService → 实例状态机 + detached 子进程 │ │ └─ LaunchStore → $DSH_HOME/mc-dev-starter/ │ │ tools: mc_projects / mc_launch*(同一套服务的薄封装) │ └───────────────────────┬───────────────────────────┘ │ detached spawn cmd.exe → gradlew.bat :fabric:runClient │ gradle daemon ──▶ java ──▶ 游戏窗口 ``` 要点: - **UI 只是读状态的壳**:所有状态在宿主进程里,客户端刷新、重开、切会话都不丢。 - **同源请求**:Electron 外壳的页面是 `dsh-app://app/`,外壳把非静态路径转发给宿主,所以客户端必须用**相对路径** `/mcdev/api/...` 而不是绝对 origin,否则跨域被拒。这条来自 `dsh-better-sidebar` 的踩坑记录,直接照做。 - **实时性**:v1 用轮询(打开浮层 1s,收起 5s),M4 再评估 WebSocket 推送。 ### 5.1 包与入口 | 位置 | 入口 | 运行环境 | 作用 | |---|---|---|---| | `./client` | `lib/client.js` | 浏览器 | 胶囊、浮层、日志标签页 | | `.`(主入口) | `lib/index.js` | 宿主 Node | 路由、扫描、启动服务、工具 | | `cordis.patch.yml` | `insert` 两行 | — | 分别挂宿主与客户端 | `package.json` 声明: ```jsonc { "type": "module", "main": "lib/index.js", "exports": { ".": "./lib/index.js", "./client": "./lib/client.js" }, "dsh": { "bundle": { "patch": "./cordis.patch.yml" }, "client": { "inject": [ "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-slots", "@deepseek-ai/dsh-client-ui-conversation", "@deepseek-ai/dsh-client-modules" ], "platform": "web" } } } ``` ### 5.2 客户端侧要用的接口(已核实) ```js // 注入标题栏工具区;声明可能晚于插件激活,所以用 slots.inject 等它 export const inject = ['slots', 'locale', 'modules'] ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'mc-dev-starter:run-capsule', order: 15, registrant: 'minecraft-dev-starter', inject: () => ({ /* 我们的 store / api */ }), }, RunCapsule)) ``` 已知 slot(从已装插件里核实到的,不是猜的):`conversation.session.header.utilities`、`conversation.session.header.corner`(单占用,被内置侧栏占着)、`shell.overlay`、`sidebar.right.pane.tab`、`settings.section`。胶囊用第一个。 日志标签页走右侧栏自己的两个注册面(不需要碰 slot 的名字): ```js // 1) 声明一种标签类型 const disposeType = ctx.effect(() => ctx.sidebarRightTabs.register({ id: 'minecraft-dev-starter/logs', kind: 'mcdev.logs', title: () => '运行日志', })) // 2) 声明这种标签的内容;body 通过 useTabInfo() 拿 { sidebar, panel, tab } ctx.slots.register({ name: 'sidebar.right.pane.tab', key: 'minecraft-dev-starter/logs' }, LogBody) // 3) 打开(会自动展开右侧栏,这是它文档里明说的行为) ctx.sidebarRight.openTab('mcdev.logs') ``` `ctx.sidebarRight` 还有 `isExpanded()` / `toggleExpanded()` / `close(tabId)` / `active()`;`openResource` 用不到。 ### 5.3 宿主侧要用的接口(已核实) ```js ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: '/mcdev/api', handler: async (req, res) => { /* POST /mcdev/api/ → JSON */ }, }), 'mcdev: /mcdev/api routes') ``` `ctx.webServer` 由 `dsh-host-webserver` 提供,web profile 里已在。方法集合: | 方法 | 用途 | |---|---| | `projects.scan` | 当前 cwd 的工程与目标列表 | | `launch.start` | 启动(工程 + 模块 + 目标) | | `launch.stop` | 停止(按工程) | | `launch.current` | 当前状态(胶囊轮询用,越小越好) | | `launch.history` | 这个工程的历史运行(日志切换用) | | `launch.logs` | 按 launchId 增量读日志 | ### 5.4 客户端产物的加载协议(已核实) DSH 不按普通 ESM 加载客户端包,而是要求包自己注册到模块表: ```js window.__ModuleLoader__.load({ id: 'minecraft-dev-starter', factory: (require) => { var module = { exports: {} } var exports = module.exports // …CJS 体,外部依赖走 require('react') 之类… exports.inject = ['slots', 'locale', 'sidebarRight', 'sidebarRightTabs'] exports.apply = apply return module.exports }, }) ``` 从 `dsh-better-sidebar` 的成品里读出来的事实: - 外部依赖只有这几个:`react`、`react/jsx-runtime`、`react-dom`、`react-dom/client`、`@deepseek-ai/dsh-client-ui-primitives`,全走 `require`。 - CSS 不是单独文件,是构建期内联成字符串,运行时 `document.createElement('style')` 注入。我们干脆不引入 CSS modules,直接用模板字符串 + 一次性注入,省一层工具链。 - 导出的是 cordis 插件形状(`inject` + `apply`)。 --- ## 6. 工程识别(地基) ### 6.1 扫描策略 - 根:会话 cwd(`exec.agent.session.header.cwd`),不跨出工作区。 - 深度:默认 3,BFS;剪枝 `.git .gradle build out bin run .idea node_modules libs reference .dsh`。 - 候选:存在 Gradle 构建文件、loader 元数据文件或 `gradlew` 的目录。 - 多模块:读 `settings.gradle(.kts)` 的 `include` 展开子项目。 - 预算:≤2000 目录、≤120 候选,触顶返回 `truncated: true`(不静默截断)。 剪枝不是洁癖:`tmachines/reference/` 下有 3 个依赖的 `mods.toml`,不剪就会把 Create 认成本工程的元数据。 ### 6.2 判定表 | 识别为 | 强特征 | 元数据文件 | 版本来源 | |---|---|---|---| | Fabric | `fabric-loom` | `src/main/resources/fabric.mod.json`、`src/main/templates/fabric.mod.json` | `gradle.properties` 的 `minecraft_version` | | NeoForge | `net.neoforged.moddev`、`net.neoforged.gradle.userdev` | `META-INF/neoforge.mods.toml`、`mods.toml` | `minecraft_version`;`neo_version` 前缀映射(21.1→1.21.1) | | Forge | `net.minecraftforge.gradle`、`forgegradle` | `META-INF/mods.toml`、`mcmod.info` | 构建脚本 `minecraft { version }`、`gradle.properties` | | Paper | `paperweight`、`io.papermc.paper` | `paper-plugin.yml`、`plugin.yml` | `api-version`、`paper_api_version` | | Spigot | `spigot-api` | `plugin.yml` | `api-version` | | Architectury | `architectury`、`dev.architectury.loom` | 子项目各自元数据 | 子项目汇总 | | 未知 Gradle | 有构建文件,以上都不命中 | — | 只报 Gradle 版本 | - **占位符**:元数据普遍写 `${mod_id}`,用 `gradle.properties` 建 `key→value` 表替换;替换不掉的保留原文并标 `unresolved`。 - **置信度**:0.95(强特征 + 元数据一致)/ 0.8(强特征无元数据)/ 0.5(仅弱特征)/ 0.2(不结论)。矛盾时降分并写 `conflicts[]`。 ### 6.3 可运行目标(浮层里显示的东西) 启动目标 = **(模块, Gradle 任务)** 二元组,不是单纯的"Client"。 | 检测到的插件 | 任务命名规则 | 常见目标 | |---|---|---| | fabric-loom | `runs { client { client() } }` → `runClient` | `Client` / `Server` | | net.neoforged.moddev | `runs { client { client() } }` → `runClient` | `Client` / `Server` / `Data` / `GameTestServer` | | FG6(1.20.1) | `runs { client {} }` → `runClient` | `Client` / `Server` / `Data` | | Paper / Spigot | 无本地运行任务 | 显示"需构建后用服务器运行" | **任务名从 runs 块读,不靠固定清单**:块名 `client` → 任务 `runClient`(首字母大写拼 `run`),块里是 `client()` / `server()` / `data()` / `gameTestServer()` 之一。读不到 runs 块时,只对"插件默认会生成"的组合给候选并标 `guessed: true`,浮层上显示为灰字+"未验证"。 ### 6.4 多模块与 Architectury(专门处理) 现实情况(本机 `Custom-Machinery-1.20.1` 实测): - 根 `build.gradle` 用 `subprojects { apply plugin: "dev.architectury.loom" }`,loom 落到 `common` / `fabric` / `forge` 三个模块上。 - 三个模块的 `build.gradle` 都是 `loom { ... }`,但**没有一个声明 `runs {}`**。 - 因此"到底有没有 `:fabric:runClient`"静态读不出来。 处理办法: 1. **模块识别**:`settings.gradle(.kts)` 的 `include("fabric")` → 模块列表;每个模块记录 `projectPath`(`":"`、`":fabric"`)与 `dir`。 2. **任务归属**:runs 块写在哪个模块的 `build.gradle` 里,任务就归哪个模块;根工程的任务路径不带前缀(`runClient`),子模块的是 `:fabric:runClient`。 3. **调用方式**:始终在**工程根**执行 `gradlew :fabric:runClient`(而不是切到模块目录跑),这样 Gradle 项目模型一致、daemon 复用、`gradle.properties` 也是根的那份。 4. **静态读不到时的兜底**:给该模块的候选任务标 `guessed`,浮层里显示为灰字。本机 `common` 模块通常没有客户端(architectury 的 common 是公共代码),所以默认不猜 `common`。 5. **让用户验证一次**:浮层底部提供「验证可运行目标」,后台跑一次 `gradlew ::tasks --all` 并缓存结果(按 `build.gradle` / `gradle.properties` 的 mtime + wrapper 版本失效)。 - 为什么不做成自动:本机实测这条命令花了 **6 分 37 秒**,最后还因为缺 `dev.latvian.mods:kubejs` 依赖失败。放在扫描路径上会让面板一打开就卡住。 - 探测失败时把 Gradle 的原话(`Could not find …`)原样展示——这本身就是有用的信息。 6. **JDK 按模块算**:architectury 的 `common` 与平台模块可能要求不同 JDK;模块级 toolchain 声明优先于工程级。 ### 6.5 环境诊断 - Java 需求:1.7.10/1.12.2→8,1.16.5/1.20.x→17,1.21.x→21,26.x→25;toolchain 声明优先。 - JDK 探测:项目配置 > `org.gradle.java.home` > `JAVA_HOME` > `PATH` > 常见安装目录。 - 风险项:JDK 与 MC 不匹配、wrapper 的 Gradle 不支持该 JDK、缺 wrapper、首次运行要下载资源。 --- ## 7. 启动与托管(地基) ### 7.1 进程模型 **不用 `ctx.subprocess`**(服务 dispose 会杀光托管进程)、**不用 `ctx.jobs`**(owner 释放即取消,游戏不是任务)。用 `child_process.spawn({ detached: true, windowsHide: true })` + `unref()` + 自管 supervisor。 - 状态机:`starting → running → exited(normal|crashed|killed|failed)`。 - 宿主退出不影响游戏;宿主重启后按 `pid + 启动时间` 对账,探不到就标 `unknown`,不伪造 `exited`。 - 就绪判定:客户端命中 `Backend library: LWJGL` / `OpenAL initialized`;服务端命中 `Done (...)! For help, type`。 - 一键动作:`▶` = 直接跑(Gradle 自带增量编译);`⟳` = `clean` 后跑(用于"改了 mixin 不生效"这类情况)。 ### 7.2 Windows 细节 | 项 | 处理 | |---|---| | 调用 | `cmd.exe /d /s /c ""C:\proj\gradlew.bat" runClient"` + `windowsVerbatimArguments: true` | | 停止 | `taskkill /PID /T /F`,`/T` 不能少 | | 输出编码 | 中文 Windows 下 Gradle 可能输出 GBK;按替换字符比例判定后回退 `TextDecoder('gbk')` | | 路径 | 全绝对路径 + 引号;含空格与中文的目录进回归集 | ### 7.3 日志与归因 - 落盘 `$DSH_HOME/mc-dev-starter/logs/.log`(原样,不插时间戳),内存保留 2000 行环形缓冲。 - 归因规则:`BUILD FAILED`+`Could not resolve`(依赖)、`Unsupported class file major version`(JDK)、`No matching toolchains found`(缺 JDK)、`OutOfMemoryError`(jvmargs)、`Address already in use`(端口)、`Mixin apply failed`(注入)、退出码 0 但有新 `crash-reports/*.txt`。 - 归因结论同时出现在浮层顶部和 AI 工具结果里——同一份判定。 ### 7.4 并发 **每个工程最多一个实例**,跨工程可以并存。同工程内换目标必须先停——这条在服务侧强制,不靠界面自觉。 跨工程并存只在浮层里提示内存(每个客户端 2-4G);端口冲突由归因规则兜住。 --- ## 8. AI 工具(次要) 沿用同一套服务,薄封装成 4 个工具(从 v0.1 的 6 个收敛): | 工具 | 用途 | |---|---| | `mc_projects` | 列出当前工作区识别到的工程与可运行目标 | | `mc_launch` | 启动某个目标(默认等就绪后返回,不占死回合) | | `mc_launch_logs` | 增量读日志(含归因结论) | | `mc_launch_stop` | 停止实例 | 描述里写明分工:**构建/测试用 `mc_gradle`,运行用 `mc_launch`**。 --- ## 9. 配置与状态 部署配置(patch 行 `config`): ```yaml readyTimeoutMs: 90000 # 等就绪标记的上限 defaultGraceMs: 8000 # 停止时的宽限 logRingLines: 2000 # 每个实例内存里保留的行数 pollIntervalMs: 1000 # 浮层/胶囊轮询间隔 autoOpenLogTab: true # 启动时自动打开右侧栏日志标签 logHistoryLimit: 100 # 历史运行记录条数(滚动淘汰) javaHomes: [] # 额外 JDK 搜索目录 ``` **没有项目级配置文件**。v0.2 里的「编辑配置…」已去掉:多一个文件、多一处可能写错的地方,收益不明显。要调 JDK 之类的,走部署配置或环境变量。 状态:`$DSH_HOME/mc-dev-starter/state.json`,存实例记录(上限 `logHistoryLimit`)与扫描缓存指纹;日志按 `.log` 同目录存放。 --- ## 10. 范围边界 本插件面向 **Gradle 工程**:识别工程画像、启动 `run*` 任务、解释运行状态、给一次性任务一个结论。 `build` / `test` / 依赖同步这类 Gradle 自身的通用能力不在范围内——那是另一个插件的位置(见 §16.2)。 --- ## 11. 仓库与开发流程 ``` minecraft-dev-starter/ ├─ doc/ 策划、调研 ├─ src/ │ ├─ index.js 宿主入口:路由 + 服务 + 工具 │ ├─ lib/ │ │ ├─ scanner.js 识别规则引擎 │ │ ├─ profiles.js loader 判定、版本映射、run 目标 │ │ ├─ modules.js 多模块 / Architectury 展开与任务路径 │ │ ├─ java.js JDK 探测 │ │ ├─ spawn.js detached spawn / 树杀 / 编码 │ │ ├─ logs.js 环形缓冲 + 归因 │ │ ├─ store.js state.json │ │ ├─ api.js /mcdev/api 方法表 │ │ └─ adapters/gradle.js │ └─ client/ │ ├─ index.tsx 客户端入口(cordis 插件 + slot/tab 注册) │ ├─ Capsule.tsx 标题栏胶囊 │ ├─ Flyout.tsx 目标单选浮层 │ ├─ LogTab.tsx 右侧栏日志标签(含历史切换) │ ├─ styles.ts CSS 字符串 + 一次性注入 │ └─ api.ts 相对路径 fetch 封装 ├─ scripts/build-client.mjs esbuild + __ModuleLoader__ 包装 ├─ cordis.patch.yml ├─ package.json └─ README.md ``` - 宿主侧纯 ESM,无编译;客户端侧用 **esbuild** 打成 `lib/client.js`(见 §16 D2)。 - `tsc --noEmit` 只做类型检查,不产出。 - 测试 `node --test`;客户端组件不做单测,靠真机冒烟。 --- ## 12. 里程碑 | 里程碑 | 内容 | 估时 | 验收 | |---|---|---|---| | **M0 UI 探针** | 最小客户端包:`__ModuleLoader__` 包装、标题栏出现按钮、右侧栏注册一个空标签、`/mcdev/api/ping` 通 | 1-1.5d | ✅ **已通过**:加载/激活/标签注册/自动展开全部实测成立,见 `AGENTS.md` 验证记录 | | M1 识别 | 规则引擎、6 类 loader、模块展开、run 目标、占位符、JDK 探测 | 2-3d | ✅ **已通过**:22 个真实工程、22 个单测,见 `AGENTS.md` 验证记录 | | M2 托管启动 | LaunchService、detached spawn、就绪判定、日志、stop、状态持久化 | 3d | ✅ **已通过**:假 gradlew 跑通启停、日志、重复启动拦截、无孤儿;见 `AGENTS.md` 验证记录 | | M3 胶囊与浮层 | 胶囊状态、单选目标、启停、按工程互斥、轮询 | 2d | ✅ **已通过**:胶囊挂载、单选与记忆、点击启停;见 `AGENTS.md` 验证记录 | | M4 日志标签 | 右侧栏标签、自动打开、历史切换、归因 | 2d | ✅ **已通过**:历史下拉、跟随、归因与 `autoOpenLogTab` 实测;见 `AGENTS.md` 验证记录 | | M5 AI 工具 + 打磨 | 4 个工具、中文文案 | 1-2d | ✅ **已通过**:工具注册与执行单测;版本对齐桌面 0.2.1-rc.3;见 `AGENTS.md` 验证记录 | | M7 UI 打磨 | 动效、空态、密度与文案 | 1-2d | ✅ **已完成**:内联图标、骨架、动效(含 reduced-motion)、文案统一;见 `AGENTS.md` 验证记录 | M0-M5 合计 11-13.5 人日。M0 如果证伪(客户端包加载不了),立即退回"独立网页面板",识别与启动的代码全部复用,只换界面壳。 --- ## 13. 测试策略 1. 规则单测:fixtures 覆盖 fabric / neoforge / forge-1.12 / paper / architectury 多模块 / unknown。 2. 真实回归:本机 17 个工程扫描快照对比(不联网、不构建);`Custom-Machinery-1.20.1` 单独断言模块展开与任务路径。 3. argv 单测:Windows 引号、空格、中文路径、`:` 前缀任务路径。 4. 进程集成:假 `gradlew.bat`(打印就绪标记后 sleep)验证 detached、就绪判定、tree kill、重启对账、按工程互斥。 5. 编码单测:GBK / UTF-8 混合输出。 6. 手动冒烟(每次发版):起一个 Fabric 客户端 + 一个 NeoForge 客户端;停止后无孤儿 java;关 DSH 再开,状态对得上;日志标签能翻到上一次运行。 --- ## 14. 风险 | 风险 | 影响 | 对策 | |---|---|---| | ~~客户端插件加载不起来~~(M0 已证伪,降级) | — | 加载协议、slot、右侧栏标签均已实测通过,见 `AGENTS.md` 验证记录 | | **slot 只在会话界面存在时可见** | 空会话/新手引导页看不到胶囊 | 胶囊设计成"跟随会话";无会话时不做假渲染,UI 上不承诺全局入口 | | slot / 服务名随 DSH 版本漂移 | 胶囊消失或报错 | peer 版本对齐已装插件;slot 名与服务名收敛到常量;`client.report` 通道可随时复验 | | 构建插件与产品平台不是一回事 | 把 Forge 模组判成 NeoForge | 按插件判(如 `net.neoforged.moddev.legacyforge` 产物是 Forge),不按组织名判;M1 已实测修正 | | 换会话后胶囊状态错乱 | 点错工程 | 胶囊按 cwd 绑定,切换时重建;运行中实例不再挂在新会话上 | | 进程树杀不干净 | 残留 java 吃内存 | `taskkill /T /F` + 冒烟专查孤儿 | | 识别误判 | 用户信任崩塌 | 结论附证据;矛盾降置信度;`unknown` 是合法答案 | | 多模块误判(把 common 当可运行模块) | 点了没反应 | `guessed` 标记 + 「验证可运行目标」一次;architectury 默认不猜 `common` | | 探测 Gradle 又慢又可能失败 | 面板卡住 / 用户以为坏了 | 探测只由用户触发、后台跑、按 mtime 缓存;失败原样展示 Gradle 报错 | | 首跑下载慢被误判为失败 | 用户以为坏了 | 区分"构建中/下载中"与"已失败",延长等待并提示 | | debug 需求落空 | 用户不满 | 见 D4:先不做,用 JDWP 参数 + 外部调试器兜底 | --- ## 15. 安全 - 只启动工作区内识别到的工程,路径必须落在会话 cwd 之下。 - 启动参数只有"工程 + 模块 + 目标",不接受界面自由输入的 task 名或 shell 字符串;argv 全程结构化。 - 状态与日志只写 `$DSH_HOME/mc-dev-starter/`,**不往用户工程里写任何文件**。 --- ## 16. 已定 / 待定 ### 16.1 已定 | # | 决策 | 结论 | 依据 | |---|---|---|---| | D1 | 包名 / id | `minecraft-dev-starter`;slot id `mc-dev-starter:run-capsule`;日志标签 kind `mcdev.logs` | 与既有 `mc_*` 工具前缀一致 | | D2 | 客户端构建链 | **esbuild**:`format=cjs` + 外部依赖 + 自写 `__ModuleLoader__` 包装脚本 | 见 16.3 的对比 | | D3 | 日志落点 | 右侧栏标签页 + 标签内历史切换,默认最新;启动时自动打开(`autoOpenLogTab`) | `ctx.sidebarRight.openTab` 打开即展开 | | D4 | debug 按钮 | v1 不做;需要调试时启动参数加 JDWP,用 IDEA attach | Gradle 的 `runClient` 是 JavaExec,命令行塞不进游戏 JVM 的 jvmArgs | | D5 | 服务端 | 一并做(`Server` 目标同机制) | 目标列表本来就从 runs 块读 | | D6 | 数据位置 | `$DSH_HOME/mc-dev-starter/` | 不污染用户工程 | | D7 | 重新构建后运行 | 不做。要 clean 自己先跑 | 避免点一下等十分钟 | | D8 | 编辑配置 | 不做,也没有项目级配置文件 | 少一个入口与一类误操作 | | D9 | i18n | 中文优先,不接多语言 | 先服务单人使用 | | D10 | 并发 | 每工程一个实例,跨工程可并存 | 你确认的场景 | | D11 | 多模块 | 按模块分组、单选一个目标 | 你确认的方案 | ### 16.2 待定 | # | 决策 | 建议 | 备选 | |---|---|---|---| | D12 | 是否做通用启动器(实例库 / 直启 java) | 不做;本插件只面向 Gradle 工程 | — | | D13 | 是否给面板加键盘快捷键(启动/停止) | M5 后再看,先不做 | 现在就接 `dsh-client-shortcuts` | | D14 | 目标验证结果要不要跨机器共享 | 不要,缓存只在本机 | 写进工程(会污染仓库) | ### 16.3 为什么是 esbuild 而不是 tsdown 产物要求(从 `dsh-better-sidebar/lib/client.js` 反读,见 §5.4):必须是 `window.__ModuleLoader__.load({id, factory})` 包起来的 CJS 体,外部依赖走 `require`,CSS 内联成字符串。 | | esbuild | tsdown | |---|---|---| | 产物形态 | `format: 'cjs'` 直接得到 `require(...)` 调用,包一层 wrapper 就完事 | rolldown 默认 ESM;要产出 `__ModuleLoader__` 形态得写/抄插件 | | wrapper 从哪来 | 我们自己在 `scripts/build-client.mjs` 里拼 banner/footer,20 行看得懂 | `dsh-better-sidebar` 用了它,但它的 `tsdown.config` 没随包发布,得逆向 | | TSX / JSX | 内置 | 内置 | | CSS | 我们不引入 CSS 方案,直接字符串注入,esbuild 不参与 | 它的 CSS modules 支持更好,但我们用不上 | | 速度 | 毫秒级,`--watch` 够用 | 也快,但依赖 rolldown 版本面 | | 类型检查 | 不做,单跑 `tsc --noEmit` | 同样不做 | | 风险 | 我们自己控制 wrapper,出问题好查 | 黑盒部分多,M0 排错更费时 | M0 的探针会先把这个 wrapper 跑通;如果 esbuild 这条路有意外,再退 tsdown,界面代码不受影响。 **M0 结果**:手写的 wrapper 已被运行时接受(`__ModuleLoader__` 加载、slot 注册、`openTab` 展开全部成立),说明"自己产 CJS + 自己包 wrapper"这条路可行;esbuild 只是把手写换成构建产物。 --- ## 附录 A:已核实事实与出处 见 `doc/调研-DSH插件机制.md`。与本版最相关的五条: - 客户端插件可注入 `conversation.session.header.utilities`,已装插件 `dsh-better-sidebar` 就是这么在标题栏放按钮的。 - 右侧栏有正式接口:`ctx.sidebarRightTabs.register({ id, kind, title })` + `ctx.slots.register({ name: 'sidebar.right.pane.tab', key: id }, Body)` + `ctx.sidebarRight.openTab(kind)`;`openTab` 会自动展开面板。 - 客户端产物必须是 `window.__ModuleLoader__.load({ id, factory })` 包装的 CJS 体,外部依赖只有 react / react-dom / jsx-runtime / `dsh-client-ui-primitives`。 - 宿主侧用 `ctx.webServer.register({ kind: 'prefix', path, handler })` 提供 HTTP 接口。 - Electron 外壳下页面是 `dsh-app://app/`,客户端必须用相对路径请求宿主,绝对 origin 会跨域失败。 ## 附录 B:本机抽样数据 `C:\Projects` 下顶层 17 个 Gradle 工程,M1 实测判定(另扫出 4 个嵌套工程): | 类型 | 数量 | 说明 | |---|---|---| | Forge | 8 | 多数是 `net.neoforged.moddev.legacyforge`——NeoForge 团队的构建插件,产物是 Forge 1.20.1 模组(`forge_version=47.x`) | | NeoForge | 5 | `net.neoforged.moddev`,如 `rationality`、`EnchatingPaper` | | Fabric | 1 | `BurstDoorOpen-fabric-1.21.1` | | Architectury | 1 | `Custom-Machinery-1.20.1`:顶层无 loom 字样,靠 `include` 展开 | | 无特征 | 2 | `AnnoCore`(loader 只在子模块里)、`Kard`(纯 Kotlin 工程) | 早期版本把 legacyforge 误计入 NeoForge(写成 13 个),M1 按"构建插件"而非"组织名"判定后改正。 `rationality` 是可用的验收样本:moddev 2.0.141 / neo 21.1.251 / MC 1.21.1 / Gradle 9.5.0 / toolchain 21,元数据在 `src/main/templates/META-INF/neoforge.mods.toml`,且开启了 configuration-cache。 多模块样本 `Custom-Machinery-1.20.1`:`settings.gradle` 里 `include("api" "common" "fabric" "forge")`,根脚本用 `subprojects { apply plugin: "dev.architectury.loom" }`,三个模块都只写了 `loom { ... }`、没有 `runs {}`。实测 `gradlew tasks --all` 耗时 6 分 37 秒并以 `Could not find dev.latvian.mods:kubejs:2001.6.4-build.121` 失败——这两条共同决定了"静态解析优先、Gradle 探测按需触发"。`TinkersAssembly` 是另一形态:单模块,moddev + architectury 插件同时存在。