minecraft-dev-starter 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -73,6 +73,7 @@ node <npm>/@deepseek-ai/dsh/lib/bin.js --profile m0test --port 3499 --no-open
73
73
  | 中文日志乱码 | `.bat` 输出按 GBK 编码 | `decodeLine`:先按 UTF-8 解,失败再按 GBK |
74
74
  | 工具报 "value is not lossless JSON" | 返回值里带了 `undefined` | 只放存在的字段;测试里用 `assertLossless()` |
75
75
  | 改 profile 后启动失败 | `package.json` 写入了 BOM | 用无 BOM 的 UTF-8 写;改完必须重启 DSH |
76
+ | 仓库的 `node_modules` 神秘消失 | 在某个 profile 里把 `link:` 依赖换成 `file:`/npm 包时,pnpm 解链会**删掉被链接目录的 `node_modules`**(实测:`plugin add link:` → 在 → `plugin add file:tgz` → 没了) | 别在同一 profile 里来回切换安装方式;tarball/registry 验证用独立 profile;发现消失就 `npm install --ignore-scripts` 重装 |
76
77
  | 侧栏标签打不开 | `openTab` 的展开是异步的 | 打开后等一帧再断言 `isExpanded()` |
77
78
 
78
79
  其他约定:
@@ -158,9 +159,38 @@ server:`Done (…s)!`);状态落盘并做重启后对账。顺带修了 GB
158
159
  - 面板改名「MC 工程」→「Mod Dev」(功能名,不是名词描述),浮层表头/标签/详情页统一。
159
160
  - 单工程工作区跳过工程列表:表头直接承担工程身份(名字 + loader/版本 + 路径)。
160
161
 
162
+ ## 发布记录
163
+
164
+ - `0.1.0` → 首版。
165
+ - `0.1.1` → 修「最小 profile 起不来」:`webServer` 从 `inject` 里拿掉,改用
166
+ `ctx.inject(['webServer'], …)` 可选等待。原来在没有 web 层的 profile(只有 `dsh-base`)里会
167
+ `pending (waiting for service: webServer)`,整个 plugin tree 加载失败。
168
+ - `0.1.2` → 修「装 0.1.1 却打印 0.1.0」:`VERSION` 改为启动时读 `package.json`。
169
+
170
+ 发版注意:
171
+
172
+ - 账号用的是 **granular token(只允许暂存)**,直接 `npm publish` 会被拒为 `E_STAGE_REQUIRED`。
173
+ 流程是 `npm stage publish` → `npm stage list`(等 `status: validated`)→
174
+ `npm stage approve <stage-id>`(人工过 2FA)。token 不能批准,批准必须是人。
175
+ - 发布前必须 `npm run build`:`lib/client.js` 是提交进仓库的产物,忘了构建就会发出旧客户端。
176
+ - 新版本发出去后,用干净 profile 装一次并看启动日志里的 `host ready (x.y.z)`,
177
+ 这条同时验证了「版本号读对了」与「包结构完整」。
178
+
179
+ ## 安全告警(npm / Socket)
180
+
181
+ npm 包页面上会出现两条告警,都来自 peer 依赖图,不是本包代码:
182
+
183
+ - **High CVE**:`zod@<4.6.6`(GHSA-mrhx-c7xc-m3w5),经 `@deepseek-ai/dsh-llm` 传导,
184
+ 属于官方运行时 `@deepseek-ai/dsh-tools` 的树。本包 `dependencies` 为空,改不了;只能等上游升 zod。
185
+ - **Uses eval**:**误报**。`@deepseek-ai/dsh-ptc-runtime/lib/index.js` 里有一张保留字表,
186
+ 其中一项就是字符串 `"eval"`,静态扫描按字符串命中了。本仓库与产物里 `\beval\b` 零命中(可复查)。
187
+
188
+ 结论:不要为了这两条去改依赖声明(把 peer 改成 optional 也挡不住 CVE,因为它是真实的传递依赖)。
189
+ 需要清除误报就往 Socket 的 false positive 通道提;需要修 CVE 就等官方升 `zod`。
190
+
161
191
  ## 还没做的事
162
192
 
163
193
  - **M6 通用启动器**:HMCL 实例库 + 直启 java,让面板也能起非 Gradle 工程(估 5–8 天)。
164
- - **发布**:目前只在本地 `link:` 安装。发布流程见 README 的「发布」一节。
165
194
  - 跨 profile 共用 `~/.dsh/mc-dev-starter/state.json`:不同 profile 的实例记录会混在一起,
166
195
  日志标签已按工程过滤,暂不影响使用,但值得按 profile 分目录。
196
+ - 桌面版目前仍用 `link:` 安装(改代码即时生效);切到 npm 版本需要每次改完都发版。
package/README.md CHANGED
@@ -1,124 +1,125 @@
1
- # minecraft-dev-starter
2
-
3
- 在 DSH(DeepSeek Harness)的会话标题栏放一个 IDEA 风格的运行控件:认出工作区里的 Minecraft 模组工程,点一下就能起 Client,日志就在右侧栏。
4
-
5
- 给做模组的人用,不是给 AI 用的——AI 侧只是顺带留了几个工具。
6
-
7
- ## 能做什么
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/Chemiofitor4096/minecraft-dev-starter/main/assets/banner.png" alt="minecraft-dev-starter" width="100%">
3
+ </p>
8
4
 
9
- **识别**:Fabric、Forge、NeoForge、Architectury、Paper、Spigot,覆盖 1.7.10 到 26.x。
10
- 单模块、多模块、聚合仓库都能认;loader 写在子模块里也能认。
11
-
12
- **启动**:Client / Server / Data / GameTestServer。自动挑 JDK(读 `<jdk>/release`,不乱起进程);
13
- 构建完能判断"进游戏了"而不是"进程还活着";关掉游戏后状态同步;宿主重启后按 pid 对账,不伪造结束原因。
5
+ # minecraft-dev-starter
14
6
 
15
- **看日志**:右侧栏独立标签,按工程列历史、默认显示最新、可跟随尾部;
16
- 崩溃时给一句归因(依赖没解析、JDK 版本不对、端口被占、认证失败等 8 类)。
7
+ 为 DeepSeek Harness 添加 Minecraft 模组开发运行控件。
17
8
 
18
- **多工程与多会话**:工作区里每个工程分别记住自己选的目标;切换对话即切换工作区,不需要手动刷新。
9
+ 插件识别当前工作区中的 Minecraft 模组与插件工程,在会话标题栏提供运行目标的选择与启动、停止操作,并将运行日志输出到侧边栏标签页。
19
10
 
20
- **AI 工具**(可选):`mc_projects` / `mc_launch` / `mc_launch_logs` / `mc_launch_stop`,与面板共用同一套服务。
11
+ ## 功能
21
12
 
22
- **better-sidebar**:装了 [dsh-better-sidebar](https://github.com/topics/dsh-better-sidebar),还会多出两个带图标的侧栏页面。
13
+ - **工程识别**:支持 Fabric、Forge、NeoForge、Architectury、Paper 与 Spigot,覆盖 Minecraft 1.7.10 至 26.x。支持单模块、多模块与聚合仓库;loader 声明位于子模块时同样可以识别。
14
+ - **运行目标**:从 Gradle 配置中提取 Client、Server、Data、GameTestServer 等任务。按 loader 惯例推测的目标会标记为未验证,不影响选择与启动。
15
+ - **托管启动**:通过 Gradle Wrapper 启动任务,依据日志标记判定是否进入运行状态,而非仅依据进程存活。停止时终止 Gradle 及其全部子进程。
16
+ - **状态保持**:运行记录持久化到本地。宿主重启后按进程 ID 校验:确认存活则继续跟踪,无法确认则记为未知,不推测退出原因。
17
+ - **运行日志**:独立侧边栏标签页,按工程展示启动历史,支持跟随输出。日志按级别着色:WARN 与 ERROR 高亮,堆栈帧与 Gradle 任务行区分显示,时间、线程与 logger 弱化。异常退出时给出可能原因,覆盖依赖解析失败、JDK 版本不匹配、端口占用、认证失败等情况。
18
+ - **工作区跟随**:以当前会话的工作目录作为工作区,切换会话时自动重新扫描,无需手动刷新。
19
+ - **模型工具**(可选):提供 `mc_projects`、`mc_launch`、`mc_launch_logs`、`mc_launch_stop` 四个工具,与面板共用同一套服务。
20
+ - **扩展集成**:安装 [dsh-better-sidebar](https://github.com/topics/dsh-better-sidebar) 后,面板与日志可在其侧边栏中作为带图标的标签页打开。
23
21
 
24
22
  ## 安装
25
23
 
26
- 需要 DSH 桌面版(0.2.1 及以上)或 dsh CLI。
24
+ 要求 DSH 桌面版 0.2.1 或更高版本,或对应的 `dsh` CLI。
27
25
 
28
26
  ```powershell
29
- # 本地仓库(改完即生效)
30
- dsh plugin --profile desktop add link:C:/Projects/minecraft-dev-starter
27
+ # 从 npm 安装
28
+ dsh plugin --profile desktop add minecraft-dev-starter
31
29
 
32
- # 打好的 tarball
33
- dsh plugin --profile desktop add file:./minecraft-dev-starter-0.1.0.tgz
30
+ # 本地仓库(修改后即时生效)
31
+ dsh plugin --profile desktop add link:C:/Projects/minecraft-dev-starter
34
32
 
35
- # 从 npm 安装(发布之后)
36
- dsh plugin --profile desktop add minecraft-dev-starter
33
+ # 使用打包文件
34
+ dsh plugin --profile desktop add file:./minecraft-dev-starter-0.1.3.tgz
37
35
  ```
38
36
 
39
- 装完**重启 DSH** 生效。卸载:
37
+ 安装完成后需重启 DSH 生效。卸载:
40
38
 
41
39
  ```powershell
42
40
  dsh plugin --profile desktop remove minecraft-dev-starter
43
41
  ```
44
42
 
45
- `dsh plugin` 实际是 pnpm 的转发器:它把包装进 profile、再把声明了 `dsh.bundle` 的依赖加进
46
- `dsh.profile.bundles` 层列表。所以手工在 profile 的 `package.json` 里加一行再用 pnpm 安装,
47
- 效果是一样的。
43
+ `dsh plugin` 是 pnpm 的封装:将插件加入 profile 的依赖,并把声明了 `dsh.bundle` 的包加入 `dsh.profile.bundles` 列表。直接编辑 profile 的 `package.json` 后执行 `pnpm install`,效果相同。
48
44
 
49
45
  ## 使用
50
46
 
51
- 1. 打开一个模组工程所在的对话。
52
- 2. 标题栏出现 `● Client ▶`。点名字展开浮层,选工程与目标;点 ▶ 开始构建并启动。
53
- 3. 启动后自动打开「运行日志」(可在配置里关掉),日志实时刷新。
54
- 4. 再点一次按钮(变成 ⏹)停止,会连同 Gradle 拉起的整个进程树一起结束。
47
+ 1. 打开包含 Minecraft 工程的工作区会话。
48
+ 2. 标题栏显示当前选中的运行目标。点击目标名称打开浮层,从中选择工程与任务。
49
+ 3. 点击启动按钮开始构建并运行。启动后自动打开「运行日志」标签页,该行为可通过配置关闭。
50
+ 4. 运行期间按钮切换为停止;点击后终止 Gradle 及其子进程。
55
51
 
56
- 浮层形态会自适应:工作区里只有一个工程时跳过列表,表头直接是工程名、loader/版本与路径;
57
- 有多个工程时列表固定高度,选谁只换下面的目标区,界面不会跳。
52
+ 浮层布局随工程数量变化:单工程工作区不显示工程列表,表头直接呈现工程名称、loader 与版本、路径;多工程工作区显示固定高度的列表,切换工程时仅更新下方目标区域,页面不会跳动。
58
53
 
59
54
  ## 配置
60
55
 
61
- 在 profile 的 `cordis.patch.yml` 里给插件那行加 `config`:
56
+ 在 profile 的 `cordis.patch.yml` 中为该插件条目添加 `config` 字段:
62
57
 
63
58
  ```yaml
64
59
  - id: mcdev-host
65
60
  name: minecraft-dev-starter
66
61
  config:
67
- autoOpenLogTab: true # 启动后自动打开日志标签
68
- readyTimeoutMs: 90000 # 等"进游戏"的上限,超时不算失败
69
- logRingLines: 2000 # 内存里保留的日志行数(磁盘文件是完整的)
62
+ autoOpenLogTab: true
63
+ readyTimeoutMs: 90000
64
+ logRingLines: 2000
70
65
  ```
71
66
 
72
- ## 支持范围
67
+ | 配置项 | 类型 | 默认值 | 说明 |
68
+ |---|---|---|---|
69
+ | `autoOpenLogTab` | boolean | `true` | 启动后自动打开运行日志标签页 |
70
+ | `readyTimeoutMs` | number | `90000` | 等待进入运行状态的超时时间;超时不计为失败 |
71
+ | `logRingLines` | number | `2000` | 内存中保留的日志行数;磁盘上的日志始终完整 |
73
72
 
74
- | 平台 | 版本线 | 说明 |
73
+ ## 兼容范围
74
+
75
+ | 平台 | 版本范围 | 说明 |
75
76
  |---|---|---|
76
- | Fabric | 1.14 – 26.x | loom 的 `runClient` 一族 |
77
- | Forge | 1.7.10 / 1.12.2 / 1.16.5 / 1.20.1 | 包括 legacyforge |
77
+ | Fabric | 1.14 – 26.x | loom 提供的 `runClient` 等任务 |
78
+ | Forge | 1.7.10 / 1.12.2 / 1.16.5 / 1.20.1 | 包含 legacyforge |
78
79
  | NeoForge | 1.20.1 – 26.x | moddev 插件 |
79
- | Architectury | 1.16 – 26.x | 按子模块拆目标 |
80
+ | Architectury | 1.16 – 26.x | 按子模块拆分任务 |
80
81
  | Paper / Spigot | 1.7.10 – 26.x | 服务端插件工程 |
81
82
 
82
- JDK 需求按版本推断(1.7.10/1.12.2 → 8,1.18–1.20.4 → 17,1.20.5+ → 21,26.x → 25),
83
- 和机器上已装的 JDK 比对,缺了会在面板里提示,不会带着错的版本硬跑。
83
+ JDK 版本按 Minecraft 版本推断:1.7.10 与 1.12.2 需要 Java 8,1.18 至 1.20.4 需要 Java 17,1.20.5 及以上需要 Java 21,26.x 需要 Java 25。插件将需求与已安装的 JDK 比对,缺失或版本不符时在面板中提示。
84
84
 
85
85
  ## 开发
86
86
 
87
87
  ```powershell
88
88
  npm install
89
- npm run build # 构建客户端到 lib/client.js(改 src/client 后必须跑)
89
+ npm run build # 构建客户端到 lib/client.js;修改 src/client 后必须执行
90
90
  npm test # 59 个测试
91
91
  npm run build -- --watch
92
92
  ```
93
93
 
94
- 仓库结构、硬坑清单与历史验证结论都在 [AGENTS.md](./AGENTS.md)。
95
- 设计文档:[策划](./doc/策划.md) · [DSH 插件机制调研](./doc/调研-DSH插件机制.md)。
94
+ 仓库结构、已知问题与历次验证结论见 [AGENTS.md](./AGENTS.md)。设计文档:[策划](./doc/策划.md)、[DSH 插件机制调研](./doc/调研-DSH插件机制.md)。
96
95
 
97
96
  ## 发布
98
97
 
99
- DSH 插件的分发就是 npm 包 + 一份清单声明,没有单独的注册流程:
98
+ DSH 插件以 npm 包形式分发,不需要独立注册流程。
99
+
100
+ 1. **清单**:`package.json` 声明 `dsh.bundle.patch`(指向 `cordis.patch.yml`);提供浏览器界面时同时声明 `dsh.client.platform`。宿主依据这两项识别插件。
101
+ 2. **产物**:执行 `npm run build` 生成 `lib/client.js`,并将该文件提交到仓库。运行时直接读取产物,不安装依赖,也不执行构建。
102
+ 3. **校验**:执行 `npm test`;使用 `npm pack --dry-run` 确认包内容(`files` 字段已限定为 `src`、`lib`、`cordis.patch.yml`、README 与 AGENTS)。
103
+ 4. **发布**:`npm publish --access public`。若使用仅允许暂存的 granular token,改为以下流程:
104
+
105
+ ```powershell
106
+ npm stage publish # 提交到暂存区
107
+ npm stage list # 等待状态变为 validated
108
+ npm stage approve <stage-id> # 人工完成 2FA 确认
109
+ ```
100
110
 
101
- 1. **清单**:`package.json` 里声明 `dsh.bundle.patch`(指向 `cordis.patch.yml`)与
102
- `dsh.client.platform`(有客户端时)。宿主靠这两项决定"这是不是一个 DSH 插件"。
103
- 2. **产物**:`npm run build` 把客户端打进 `lib/client.js` 并**提交进仓库**——
104
- 运行时直接读它,不装依赖也不做构建。
105
- 3. **自检**:`npm test`,`npm pack --dry-run` 看包里有没有多余文件(`files` 字段已限定
106
- `src`、`lib`、`cordis.patch.yml`、README、AGENTS)。
107
- 4. **发布**:`npm publish --access public`。版本号走 semver,破坏性改动升主版本。
108
- 5. **兼容性**:宿主 API 会演进(本插件按 0.2.1-rc.3 的 `dsh-tools` 开发)。发布前把
109
- `dependencies` 里的 `@deepseek-ai/dsh-tools` 与 `peerDependencies` 的 cordis 范围对齐
110
- 目标运行时;跨大版本时最好发两个版本而不是硬兼容。
111
- 6. **安装**:用户 `dsh plugin --profile <name> add <包名>` 后重启;也可以在 Plugins 面板里装。
111
+ 暂存后由人工批准是 npm 的设计:机器提交、人工确认,即使 token 泄露也无法直接发布正式版本。批准后 `latest` 标签自动指向新版本。
112
+ 5. **兼容性**:宿主 API 会持续演进。发布前将 `@deepseek-ai/dsh-tools` 与 `@deepseek-ai/cordis` 的版本范围对齐目标运行时;跨大版本时应分别发布版本,而非放宽范围强行兼容。
113
+ 6. **安装**:用户执行 `dsh plugin --profile <name> add <包名>` 后重启,或在 Plugins 面板中安装。
112
114
 
113
- 本地想先验证发布形态,可以 `npm pack` 后用 `dsh plugin add file:<tarball>` 装一遍——
114
- 它会走和 npm 安装完全相同的 pnpm 路径。
115
+ 在本地验证发布形态时,可先 `npm pack`,再通过 `dsh plugin add file:<tarball>` 安装;该路径与 npm 安装完全一致。
115
116
 
116
117
  ## 已知限制
117
118
 
118
- - 原生右侧栏的标签类型没有图标位,只有 better-sidebar 的标签能带图标。
119
- - 只支持 Gradle 工程;HMCL 实例库里的整合包还不能直接起(见 AGENTS.md 的 M6)。
120
- - 状态文件放在 `~/.dsh/mc-dev-starter/`,多个 profile 共用一份;日志标签按工程过滤,不影响使用。
121
- - Windows 上第一次构建会下载 Gradle 与依赖,可能要几分钟;之后有 daemon 会快很多。
119
+ - 原生右侧栏的标签类型不提供图标字段,图标仅支持 `dsh-better-sidebar` 中的标签。
120
+ - 当前仅支持 Gradle 工程;HMCL 实例库中的整合包尚不支持(见 AGENTS.md 的 M6)。
121
+ - 运行状态保存于 `~/.dsh/mc-dev-starter/`,多个 profile 共用同一份记录;日志标签按工程过滤,功能不受影响。
122
+ - 在 Windows 上首次构建需要下载 Gradle 与项目依赖,耗时可能达到数分钟;后续构建可使用 Gradle daemon。
122
123
 
123
124
  ## License
124
125
 
package/lib/client.js CHANGED
@@ -868,7 +868,7 @@ function DetailTab() {
868
868
  )
869
869
  )
870
870
  ],
871
- h("div", { className: "mcdev-hint", style: { marginTop: 6 } }, "\u5E26 ? \u7684\u4EFB\u52A1\u662F\u6309 loader \u60EF\u4F8B\u63A8\u6D4B\u7684\uFF0C\u672A\u7ECF\u9A8C\u8BC1\uFF1B\u542F\u52A8\u80FD\u529B\u5728 M2 \u63A5\u5165\u3002")
871
+ h("div", { className: "mcdev-hint", style: { marginTop: 6 } }, "\u5E26 ? \u7684\u4EFB\u52A1\u662F\u6309 loader \u60EF\u4F8B\u63A8\u6D4B\u7684\uFF0C\u5C1A\u672A\u771F\u5B9E\u8FD0\u884C\u9A8C\u8BC1\uFF1B\u53EF\u4EE5\u76F4\u63A5\u542F\u52A8\u8BD5\uFF0C\u5931\u8D25\u65F6\u65E5\u5FD7\u91CC\u4F1A\u7ED9\u51FA\u539F\u56E0\u3002")
872
872
  ]),
873
873
  section(
874
874
  "\u8BC1\u636E",
@@ -920,6 +920,66 @@ function row(key, value) {
920
920
 
921
921
  // src/client/LogTab.js
922
922
  var import_react5 = require("react");
923
+
924
+ // src/client/logFormat.js
925
+ var ANSI = /\u001b\[[0-9;]*[A-Za-z]/g;
926
+ function seg(text2, kind) {
927
+ return { text: text2, kind };
928
+ }
929
+ function levelKind(level) {
930
+ const upper = level.toUpperCase();
931
+ if (upper === "WARN" || upper === "WARNING") return "level-warn";
932
+ if (upper === "ERROR" || upper === "FATAL" || upper === "SEVERE") return "level-error";
933
+ if (upper === "DEBUG" || upper === "TRACE" || upper === "FINE" || upper === "FINER") return "level-debug";
934
+ return "level-info";
935
+ }
936
+ var LOG4J = /^\[(\d{2}:\d{2}:\d{2})\]\s+\[([^\]]+?)\/([A-Z]+)\]\s*(?:\[([^\]]*)\]:\s*|:\s*)?(.*)$/;
937
+ var PAPER = /^\[(\d{2}:\d{2}:\d{2}) ([A-Z]+)\]:\s*(.*)$/;
938
+ var BARE_LEVEL = /^(TRACE|DEBUG|INFO|WARN|WARNING|ERROR|FATAL)\s+(.*)$/;
939
+ var STACK = /^\s+(at |\.\.\. \d+ more)/;
940
+ function highlightLine(raw) {
941
+ const line = String(raw ?? "").replace(ANSI, "");
942
+ if (line === "") return [seg("", "plain")];
943
+ if (/^Caused by:|^\s+Suppressed:/.test(line)) return [seg(line, "fail")];
944
+ if (STACK.test(line)) return [seg(line, "stack")];
945
+ const log4j = LOG4J.exec(line);
946
+ if (log4j !== null) {
947
+ const [, time, thread, level, logger, message] = log4j;
948
+ const kind = levelKind(level);
949
+ const parts = [
950
+ seg(`[${time}] `, "time"),
951
+ seg(`[${thread}/`, "thread"),
952
+ seg(level, kind),
953
+ seg(logger === void 0 ? "]: " : "] ", "thread")
954
+ ];
955
+ if (logger !== void 0) parts.push(seg(`[${logger}]: `, "logger"));
956
+ parts.push(seg(message, kind === "level-error" ? "level-error" : "message"));
957
+ return parts;
958
+ }
959
+ const paper = PAPER.exec(line);
960
+ if (paper !== null) {
961
+ const [, time, level, message] = paper;
962
+ return [
963
+ seg(`[${time} `, "time"),
964
+ seg(level, levelKind(level)),
965
+ seg("]: ", "time"),
966
+ seg(message, levelKind(level) === "level-error" ? "level-error" : "message")
967
+ ];
968
+ }
969
+ const bare = BARE_LEVEL.exec(line);
970
+ if (bare !== null) {
971
+ const [, level, message] = bare;
972
+ return [seg(`${level} `, levelKind(level)), seg(message, levelKind(level) === "level-error" ? "level-error" : "message")];
973
+ }
974
+ if (/^> Task /.test(line) || /^> Configure project /.test(line)) return [seg(line, "task")];
975
+ if (/^BUILD SUCCESSFUL/.test(line)) return [seg(line, "ok")];
976
+ if (/^BUILD FAILED/.test(line) || /^FAILURE:/.test(line)) return [seg(line, "fail")];
977
+ if (/^\* What went wrong:|^\* Where:|^\* Try:/.test(line)) return [seg(line, "level-warn")];
978
+ if (/^(Reusing configuration cache|Deprecated Gradle features|You can use)/.test(line)) return [seg(line, "muted")];
979
+ return [seg(line, "plain")];
980
+ }
981
+
982
+ // src/client/LogTab.js
923
983
  var lastLogId;
924
984
  function LogTab() {
925
985
  const launches = useLaunches(1500);
@@ -1029,7 +1089,20 @@ function LogTab() {
1029
1089
  ),
1030
1090
  h("div", { className: "mcdev-hint mcdev-code" }, log.diagnosis.line)
1031
1091
  ),
1032
- log?.error === void 0 ? log === void 0 && running ? h(Skeleton, { rows: 4, hint: "\u6B63\u5728\u8BFB\u53D6\u65E5\u5FD7\u2026" }) : h("pre", { ref: body, className: "mcdev-logs mcdev-logs-full" }, (log?.lines ?? []).join("\n") || "\uFF08\u8FD8\u6CA1\u6709\u8F93\u51FA\uFF09") : h("div", { className: "mcdev-error", style: { marginTop: 8 } }, log.error),
1092
+ log?.error === void 0 ? log === void 0 && running ? h(Skeleton, { rows: 4, hint: "\u6B63\u5728\u8BFB\u53D6\u65E5\u5FD7\u2026" }) : h(
1093
+ "pre",
1094
+ { ref: body, className: "mcdev-logs mcdev-logs-full" },
1095
+ (log?.lines ?? []).length === 0 ? "\uFF08\u8FD8\u6CA1\u6709\u8F93\u51FA\uFF09" : (log?.lines ?? []).map(
1096
+ (line, index) => h(
1097
+ "span",
1098
+ { key: index, className: "mcdev-log-line" },
1099
+ highlightLine(line).map(
1100
+ (part, partIndex) => h("span", { key: partIndex, "data-kind": part.kind }, part.text)
1101
+ ),
1102
+ "\n"
1103
+ )
1104
+ )
1105
+ ) : h("div", { className: "mcdev-error", style: { marginTop: 8 } }, log.error),
1033
1106
  h(
1034
1107
  "div",
1035
1108
  { style: { marginTop: 8 } },
@@ -1302,6 +1375,20 @@ var CSS = `
1302
1375
  .mcdev-diagnosis-hint { color: var(--dsw-alias-state-error-primary); font: var(--dsw-font-xxs-strong-12); }
1303
1376
  .mcdev-logs-full { flex: 1 1 auto; min-height: 0; max-height: none; margin: 0; }
1304
1377
 
1378
+ /* \u65E5\u5FD7\u9AD8\u4EAE\uFF1A\u53EA\u6309\u8BED\u4E49\u4E0A\u8272\uFF0C\u65F6\u95F4/\u7EBF\u7A0B/logger \u4E00\u5F8B\u538B\u5230 tertiary\uFF0C\u8BA9\u6B63\u6587\u66F4\u6E05\u695A */
1379
+ .mcdev-log-line { display: inline; }
1380
+ .mcdev-log-line [data-kind="time"],
1381
+ .mcdev-log-line [data-kind="thread"],
1382
+ .mcdev-log-line [data-kind="logger"] { color: var(--dsw-alias-label-tertiary); }
1383
+ .mcdev-log-line [data-kind="muted"] { color: var(--dsw-alias-label-tertiary); }
1384
+ .mcdev-log-line [data-kind="level-warn"] { color: var(--dsw-alias-state-warn-primary); }
1385
+ .mcdev-log-line [data-kind="level-error"] { color: var(--dsw-alias-state-error-primary); }
1386
+ .mcdev-log-line [data-kind="level-debug"] { color: var(--dsw-alias-label-tertiary); }
1387
+ .mcdev-log-line [data-kind="task"] { color: var(--dsw-alias-label-secondary); }
1388
+ .mcdev-log-line [data-kind="ok"] { color: var(--dsw-alias-state-success-primary); }
1389
+ .mcdev-log-line [data-kind="fail"] { color: var(--dsw-alias-state-error-primary); font: var(--dsw-font-xxxs-strong-11); }
1390
+ .mcdev-log-line [data-kind="stack"] { color: var(--dsw-alias-label-secondary); }
1391
+
1305
1392
  .mcdev-section { margin-top: 12px; }
1306
1393
  .mcdev-section > h4 {
1307
1394
  margin: 0 0 4px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-dev-starter",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "DSH plugin that detects Minecraft mod projects and launches the client from the session header, with live logs in the sidebar",
6
6
  "type": "module",
@@ -44,13 +44,20 @@
44
44
  "test": "node --test \"test/*.test.js\""
45
45
  },
46
46
  "license": "MIT",
47
- "dependencies": {
48
- "@deepseek-ai/dsh-tools": "0.2.1-rc.3"
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/Chemiofitor4096/minecraft-dev-starter.git"
50
+ },
51
+ "homepage": "https://github.com/Chemiofitor4096/minecraft-dev-starter#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/Chemiofitor4096/minecraft-dev-starter/issues"
49
54
  },
50
55
  "peerDependencies": {
51
- "@deepseek-ai/cordis": "^4.0.1"
56
+ "@deepseek-ai/cordis": "^4.0.1",
57
+ "@deepseek-ai/dsh-tools": "^0.2.1-rc.1"
52
58
  },
53
59
  "devDependencies": {
60
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.3",
54
61
  "esbuild": "^0.28.2"
55
62
  }
56
63
  }
@@ -158,7 +158,7 @@ export function DetailTab() {
158
158
  ),
159
159
  ),
160
160
  ]),
161
- h('div', { className: 'mcdev-hint', style: { marginTop: 6 } }, '带 ? 的任务是按 loader 惯例推测的,未经验证;启动能力在 M2 接入。'),
161
+ h('div', { className: 'mcdev-hint', style: { marginTop: 6 } }, '带 ? 的任务是按 loader 惯例推测的,尚未真实运行验证;可以直接启动试,失败时日志里会给出原因。'),
162
162
  ]),
163
163
 
164
164
  section(
@@ -9,6 +9,7 @@ import { useEffect, useRef, useState } from 'react'
9
9
  import { call } from './api.js'
10
10
  import { h } from './h.js'
11
11
  import { IconRefresh, IconWarning } from './icons.js'
12
+ import { highlightLine } from './logFormat.js'
12
13
  import { launchStatusText, pickLogTarget } from './selection.js'
13
14
  import { Skeleton } from './Skeleton.js'
14
15
  import { loadLaunches, selectedProject, useLaunches } from './state.js'
@@ -140,7 +141,22 @@ export function LogTab() {
140
141
  log?.error === undefined
141
142
  ? log === undefined && running
142
143
  ? h(Skeleton, { rows: 4, hint: '正在读取日志…' })
143
- : h('pre', { ref: body, className: 'mcdev-logs mcdev-logs-full' }, (log?.lines ?? []).join('\n') || '(还没有输出)')
144
+ : h(
145
+ 'pre',
146
+ { ref: body, className: 'mcdev-logs mcdev-logs-full' },
147
+ (log?.lines ?? []).length === 0
148
+ ? '(还没有输出)'
149
+ : (log?.lines ?? []).map((line, index) =>
150
+ h(
151
+ 'span',
152
+ { key: index, className: 'mcdev-log-line' },
153
+ highlightLine(line).map((part, partIndex) =>
154
+ h('span', { key: partIndex, 'data-kind': part.kind }, part.text),
155
+ ),
156
+ '\n',
157
+ ),
158
+ ),
159
+ )
144
160
  : h('div', { className: 'mcdev-error', style: { marginTop: 8 } }, log.error),
145
161
  h(
146
162
  'div',
@@ -0,0 +1,109 @@
1
+ /**
2
+ * 运行日志的逐行解析:把一行拆成若干片段,交给 CSS 按语义上色。
3
+ *
4
+ * 为什么不按 ANSI 上色:Gradle 在管道里默认关闭颜色,Minecraft 的 log4j 也只在
5
+ * 终端里输出色码。多数日志查看器(mclo.gs / Aternos codex)同样是先解析出 level
6
+ * 再染色,而不是解析转义序列。这里沿用同一思路,顺便把万一出现的 ANSI 码剥掉。
7
+ *
8
+ * 支持的三种真实格式:
9
+ * [01:28:45] [Render thread/WARN] [minecraft/SoundEngine]: message ← log4j
10
+ * [01:28:45 INFO]: message ← Paper
11
+ * WARN StatusConsoleListener ... ← 无时间戳的 log4j 状态行
12
+ * 另外识别 Gradle 的 `> Task`、`BUILD SUCCESSFUL`、`FAILURE:` 与堆栈帧。
13
+ *
14
+ * @module minecraft-dev-starter/client/logFormat
15
+ */
16
+
17
+ const ANSI = /\u001b\[[0-9;]*[A-Za-z]/g
18
+
19
+ /** @typedef {'time'|'thread'|'level-info'|'level-warn'|'level-error'|'level-debug'|'logger'|'message'|'task'|'ok'|'fail'|'stack'|'muted'} SegmentKind */
20
+
21
+ /**
22
+ * @param {string} text
23
+ * @param {SegmentKind} kind
24
+ * @returns {{ text: string, kind: SegmentKind }}
25
+ */
26
+ function seg(text, kind) {
27
+ return { text, kind }
28
+ }
29
+
30
+ /**
31
+ * 把 WARN / ERROR / FATAL 等归一成三档。
32
+ * @param {string} level
33
+ * @returns {SegmentKind}
34
+ */
35
+ function levelKind(level) {
36
+ const upper = level.toUpperCase()
37
+ if (upper === 'WARN' || upper === 'WARNING') return 'level-warn'
38
+ if (upper === 'ERROR' || upper === 'FATAL' || upper === 'SEVERE') return 'level-error'
39
+ if (upper === 'DEBUG' || upper === 'TRACE' || upper === 'FINE' || upper === 'FINER') return 'level-debug'
40
+ return 'level-info'
41
+ }
42
+
43
+ const LOG4J = /^\[(\d{2}:\d{2}:\d{2})\]\s+\[([^\]]+?)\/([A-Z]+)\]\s*(?:\[([^\]]*)\]:\s*|:\s*)?(.*)$/
44
+ const PAPER = /^\[(\d{2}:\d{2}:\d{2}) ([A-Z]+)\]:\s*(.*)$/
45
+ const BARE_LEVEL = /^(TRACE|DEBUG|INFO|WARN|WARNING|ERROR|FATAL)\s+(.*)$/
46
+ const STACK = /^\s+(at |\.\.\. \d+ more)/
47
+
48
+ /**
49
+ * 一行日志 → 片段数组。永远至少返回一个片段。
50
+ * @param {string} raw
51
+ * @returns {Array<{ text: string, kind: SegmentKind }>}
52
+ */
53
+ export function highlightLine(raw) {
54
+ const line = String(raw ?? '').replace(ANSI, '')
55
+ if (line === '') return [seg('', 'plain')]
56
+
57
+ if (/^Caused by:|^\s+Suppressed:/.test(line)) return [seg(line, 'fail')]
58
+ if (STACK.test(line)) return [seg(line, 'stack')]
59
+
60
+ const log4j = LOG4J.exec(line)
61
+ if (log4j !== null) {
62
+ const [, time, thread, level, logger, message] = log4j
63
+ const kind = levelKind(level)
64
+ /** @type {Array<{ text: string, kind: SegmentKind }>} */
65
+ const parts = [
66
+ seg(`[${time}] `, 'time'),
67
+ seg(`[${thread}/`, 'thread'),
68
+ seg(level, kind),
69
+ seg(logger === undefined ? ']: ' : '] ', 'thread'),
70
+ ]
71
+ if (logger !== undefined) parts.push(seg(`[${logger}]: `, 'logger'))
72
+ parts.push(seg(message, kind === 'level-error' ? 'level-error' : 'message'))
73
+ return parts
74
+ }
75
+
76
+ const paper = PAPER.exec(line)
77
+ if (paper !== null) {
78
+ const [, time, level, message] = paper
79
+ return [
80
+ seg(`[${time} `, 'time'),
81
+ seg(level, levelKind(level)),
82
+ seg(']: ', 'time'),
83
+ seg(message, levelKind(level) === 'level-error' ? 'level-error' : 'message'),
84
+ ]
85
+ }
86
+
87
+ const bare = BARE_LEVEL.exec(line)
88
+ if (bare !== null) {
89
+ const [, level, message] = bare
90
+ return [seg(`${level} `, levelKind(level)), seg(message, levelKind(level) === 'level-error' ? 'level-error' : 'message')]
91
+ }
92
+
93
+ if (/^> Task /.test(line) || /^> Configure project /.test(line)) return [seg(line, 'task')]
94
+ if (/^BUILD SUCCESSFUL/.test(line)) return [seg(line, 'ok')]
95
+ if (/^BUILD FAILED/.test(line) || /^FAILURE:/.test(line)) return [seg(line, 'fail')]
96
+ if (/^\* What went wrong:|^\* Where:|^\* Try:/.test(line)) return [seg(line, 'level-warn')]
97
+ if (/^(Reusing configuration cache|Deprecated Gradle features|You can use)/.test(line)) return [seg(line, 'muted')]
98
+
99
+ return [seg(line, 'plain')]
100
+ }
101
+
102
+ /**
103
+ * 批量解析,便于测试与 memo 化调用。
104
+ * @param {string[]} lines
105
+ * @returns {Array<Array<{ text: string, kind: SegmentKind }>>}
106
+ */
107
+ export function highlightLines(lines) {
108
+ return (lines ?? []).map(highlightLine)
109
+ }
@@ -257,6 +257,20 @@ export const CSS = `
257
257
  .mcdev-diagnosis-hint { color: var(--dsw-alias-state-error-primary); font: var(--dsw-font-xxs-strong-12); }
258
258
  .mcdev-logs-full { flex: 1 1 auto; min-height: 0; max-height: none; margin: 0; }
259
259
 
260
+ /* 日志高亮:只按语义上色,时间/线程/logger 一律压到 tertiary,让正文更清楚 */
261
+ .mcdev-log-line { display: inline; }
262
+ .mcdev-log-line [data-kind="time"],
263
+ .mcdev-log-line [data-kind="thread"],
264
+ .mcdev-log-line [data-kind="logger"] { color: var(--dsw-alias-label-tertiary); }
265
+ .mcdev-log-line [data-kind="muted"] { color: var(--dsw-alias-label-tertiary); }
266
+ .mcdev-log-line [data-kind="level-warn"] { color: var(--dsw-alias-state-warn-primary); }
267
+ .mcdev-log-line [data-kind="level-error"] { color: var(--dsw-alias-state-error-primary); }
268
+ .mcdev-log-line [data-kind="level-debug"] { color: var(--dsw-alias-label-tertiary); }
269
+ .mcdev-log-line [data-kind="task"] { color: var(--dsw-alias-label-secondary); }
270
+ .mcdev-log-line [data-kind="ok"] { color: var(--dsw-alias-state-success-primary); }
271
+ .mcdev-log-line [data-kind="fail"] { color: var(--dsw-alias-state-error-primary); font: var(--dsw-font-xxxs-strong-11); }
272
+ .mcdev-log-line [data-kind="stack"] { color: var(--dsw-alias-label-secondary); }
273
+
260
274
  .mcdev-section { margin-top: 12px; }
261
275
  .mcdev-section > h4 {
262
276
  margin: 0 0 4px;