minecraft-dev-starter 0.1.3 → 0.1.5

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
@@ -176,6 +176,18 @@ server:`Done (…s)!`);状态落盘并做重启后对账。顺带修了 GB
176
176
  - 新版本发出去后,用干净 profile 装一次并看启动日志里的 `host ready (x.y.z)`,
177
177
  这条同时验证了「版本号读对了」与「包结构完整」。
178
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
+
179
191
  ## 还没做的事
180
192
 
181
193
  - **M6 通用启动器**:HMCL 实例库 + 直启 java,让面板也能起非 Gradle 工程(估 5–8 天)。
package/README.md CHANGED
@@ -1,136 +1,129 @@
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
+ logDock: right # right:DSH 原生右侧栏;bottom:dsh-better-sidebar 的底部工作台
64
+ readyTimeoutMs: 90000
65
+ logRingLines: 2000
70
66
  ```
71
67
 
72
- ## 支持范围
68
+ | 配置项 | 类型 | 默认值 | 说明 |
69
+ |---|---|---|---|
70
+ | `autoOpenLogTab` | boolean | `true` | 启动后自动打开运行日志标签页 |
71
+ | `logDock` | `right` \| `bottom` | `right` | 日志打开位置。`bottom` 需要安装 dsh-better-sidebar;未安装时自动回退到右侧栏 |
72
+ | `readyTimeoutMs` | number | `90000` | 等待进入运行状态的超时时间;超时不计为失败 |
73
+ | `logRingLines` | number | `2000` | 内存中保留的日志行数;磁盘上的日志始终完整 |
74
+
75
+ `logDock` 只影响「自动打开」与面板里的「打开日志」按钮;无论配置为何,两个标签页都同时存在于原生右侧栏与 better-sidebar 的页签列表中,可随时手动打开。
73
76
 
74
- | 平台 | 版本线 | 说明 |
77
+ ## 兼容范围
78
+
79
+ | 平台 | 版本范围 | 说明 |
75
80
  |---|---|---|
76
- | Fabric | 1.14 – 26.x | loom 的 `runClient` 一族 |
77
- | Forge | 1.7.10 / 1.12.2 / 1.16.5 / 1.20.1 | 包括 legacyforge |
81
+ | Fabric | 1.14 – 26.x | loom 提供的 `runClient` 等任务 |
82
+ | Forge | 1.7.10 / 1.12.2 / 1.16.5 / 1.20.1 | 包含 legacyforge |
78
83
  | NeoForge | 1.20.1 – 26.x | moddev 插件 |
79
- | Architectury | 1.16 – 26.x | 按子模块拆目标 |
84
+ | Architectury | 1.16 – 26.x | 按子模块拆分任务 |
80
85
  | Paper / Spigot | 1.7.10 – 26.x | 服务端插件工程 |
81
86
 
82
- JDK 需求按版本推断(1.7.10/1.12.2 → 8,1.18–1.20.4 → 17,1.20.5+ → 21,26.x → 25),
83
- 和机器上已装的 JDK 比对,缺了会在面板里提示,不会带着错的版本硬跑。
87
+ 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
88
 
85
89
  ## 开发
86
90
 
87
91
  ```powershell
88
92
  npm install
89
- npm run build # 构建客户端到 lib/client.js(改 src/client 后必须跑)
93
+ npm run build # 构建客户端到 lib/client.js;修改 src/client 后必须执行
90
94
  npm test # 59 个测试
91
95
  npm run build -- --watch
92
96
  ```
93
97
 
94
- 仓库结构、硬坑清单与历史验证结论都在 [AGENTS.md](./AGENTS.md)。
95
- 设计文档:[策划](./doc/策划.md) · [DSH 插件机制调研](./doc/调研-DSH插件机制.md)。
98
+ 仓库结构、已知问题与历次验证结论见 [AGENTS.md](./AGENTS.md)。设计文档:[策划](./doc/策划.md)、[DSH 插件机制调研](./doc/调研-DSH插件机制.md)。
96
99
 
97
100
  ## 发布
98
101
 
99
- DSH 插件的分发就是 npm 包 + 一份清单声明,没有单独的注册流程:
100
-
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,破坏性改动升主版本。
102
+ DSH 插件以 npm 包形式分发,不需要独立注册流程。
108
103
 
109
- 如果用的是 granular token(只允许暂存),会被拒为 `E_STAGE_REQUIRED`,改走两步:
104
+ 1. **清单**:`package.json` 声明 `dsh.bundle.patch`(指向 `cordis.patch.yml`);提供浏览器界面时同时声明 `dsh.client.platform`。宿主依据这两项识别插件。
105
+ 2. **产物**:执行 `npm run build` 生成 `lib/client.js`,并将该文件提交到仓库。运行时直接读取产物,不安装依赖,也不执行构建。
106
+ 3. **校验**:执行 `npm test`;使用 `npm pack --dry-run` 确认包内容(`files` 字段已限定为 `src`、`lib`、`cordis.patch.yml`、README 与 AGENTS)。
107
+ 4. **发布**:`npm publish --access public`。若使用仅允许暂存的 granular token,改为以下流程:
110
108
 
111
109
  ```powershell
112
- npm stage publish # 提交到暂存区,拿到 stage-id
113
- npm stage list # 看到 status: validated 再批准
114
- npm stage approve <stage-id> # 需要人工过 2FA(网页确认)
110
+ npm stage publish # 提交到暂存区
111
+ npm stage list # 等待状态变为 validated
112
+ npm stage approve <stage-id> # 人工完成 2FA 确认
115
113
  ```
116
114
 
117
- 暂存 → 人工批准是 npm 的设计:机器提交、人确认,token 泄露也发不出正式版本。
118
- 批准后 `latest` 自动指向新版本,通常十几秒内可从 registry 拉到。
119
-
120
- 5. **兼容性**:宿主 API 会演进(本插件按 0.2.1-rc.3 的 `dsh-tools` 开发)。发布前把
121
- `dependencies` 里的 `@deepseek-ai/dsh-tools` 与 `peerDependencies` 的 cordis 范围对齐
122
- 目标运行时;跨大版本时最好发两个版本而不是硬兼容。
123
- 6. **安装**:用户 `dsh plugin --profile <name> add <包名>` 后重启;也可以在 Plugins 面板里装。
115
+ 暂存后由人工批准是 npm 的设计:机器提交、人工确认,即使 token 泄露也无法直接发布正式版本。批准后 `latest` 标签自动指向新版本。
116
+ 5. **兼容性**:宿主 API 会持续演进。发布前将 `@deepseek-ai/dsh-tools` 与 `@deepseek-ai/cordis` 的版本范围对齐目标运行时;跨大版本时应分别发布版本,而非放宽范围强行兼容。
117
+ 6. **安装**:用户执行 `dsh plugin --profile <name> add <包名>` 后重启,或在 Plugins 面板中安装。
124
118
 
125
- 本地想先验证发布形态,可以 `npm pack` 后用 `dsh plugin add file:<tarball>` 装一遍——
126
- 它会走和 npm 安装完全相同的 pnpm 路径。
119
+ 在本地验证发布形态时,可先 `npm pack`,再通过 `dsh plugin add file:<tarball>` 安装;该路径与 npm 安装完全一致。
127
120
 
128
121
  ## 已知限制
129
122
 
130
- - 原生右侧栏的标签类型没有图标位,只有 better-sidebar 的标签能带图标。
131
- - 只支持 Gradle 工程;HMCL 实例库里的整合包还不能直接起(见 AGENTS.md 的 M6)。
132
- - 状态文件放在 `~/.dsh/mc-dev-starter/`,多个 profile 共用一份;日志标签按工程过滤,不影响使用。
133
- - Windows 上第一次构建会下载 Gradle 与依赖,可能要几分钟;之后有 daemon 会快很多。
123
+ - 原生右侧栏的标签类型不提供图标字段,图标仅支持 `dsh-better-sidebar` 中的标签。
124
+ - 当前仅支持 Gradle 工程;HMCL 实例库中的整合包尚不支持(见 AGENTS.md 的 M6)。
125
+ - 运行状态保存于 `~/.dsh/mc-dev-starter/`,多个 profile 共用同一份记录;日志标签按工程过滤,功能不受影响。
126
+ - 在 Windows 上首次构建需要下载 Gradle 与项目依赖,耗时可能达到数分钟;后续构建可使用 Gradle daemon。
134
127
 
135
128
  ## License
136
129
 
package/lib/client.js CHANGED
@@ -238,9 +238,19 @@ async function call(method, payload) {
238
238
  return body.result;
239
239
  }
240
240
 
241
+ // src/client/dock.js
242
+ var LOG_DOCK_RIGHT = "right";
243
+ var LOG_DOCK_BOTTOM = "bottom";
244
+ function chooseLogDock(config, hasBetterSidebar) {
245
+ if (hasBetterSidebar !== true) return LOG_DOCK_RIGHT;
246
+ return config?.logDock === LOG_DOCK_BOTTOM ? LOG_DOCK_BOTTOM : LOG_DOCK_RIGHT;
247
+ }
248
+
241
249
  // src/client/runtime.js
242
250
  var DETAIL_KIND = "mcdev.detail";
243
251
  var LOG_KIND = "mcdev.logs";
252
+ var BETTER_DETAIL_ID = "minecraft-dev-starter:project";
253
+ var BETTER_LOG_ID = "minecraft-dev-starter:logs";
244
254
  var runtime = {};
245
255
  function open(kind) {
246
256
  const sidebar = runtime.ctx?.sidebarRight;
@@ -252,10 +262,25 @@ function open(kind) {
252
262
  return false;
253
263
  }
254
264
  }
265
+ function betterSidebar() {
266
+ try {
267
+ return runtime.ctx?.get?.("betterSidebar", false);
268
+ } catch {
269
+ return void 0;
270
+ }
271
+ }
255
272
  function openDetailTab() {
256
273
  return open(DETAIL_KIND);
257
274
  }
258
- function openLogTab() {
275
+ function openLogTab(config) {
276
+ const service = betterSidebar();
277
+ if (chooseLogDock(config, service !== void 0) === LOG_DOCK_BOTTOM) {
278
+ try {
279
+ service.openTab({ type: BETTER_LOG_ID, title: "\u8FD0\u884C\u65E5\u5FD7", target: "bottom" });
280
+ return true;
281
+ } catch {
282
+ }
283
+ }
259
284
  return open(LOG_KIND);
260
285
  }
261
286
 
@@ -296,15 +321,15 @@ var store = createStore({
296
321
  flyoutOpen: false,
297
322
  /** @type {Record<string, string>} 每个工程记住的目标 key */
298
323
  selection: readSelection(),
299
- /** @type {{ autoOpenLogTab?: boolean }} 宿主下发的部署配置 */
300
- config: { autoOpenLogTab: true },
324
+ /** @type {{ autoOpenLogTab?: boolean, logDock?: 'right' | 'bottom' }} 宿主下发的部署配置 */
325
+ config: { autoOpenLogTab: true, logDock: "right" },
301
326
  /** @type {string | undefined} 当前会话的工作区(由胶囊从会话上下文注入) */
302
327
  workspaceRoot: void 0
303
328
  });
304
329
  async function loadStateInfo() {
305
330
  try {
306
331
  const info = await call("state.info", {});
307
- store.set({ config: { autoOpenLogTab: true, ...info?.config ?? {} } });
332
+ store.set({ config: { autoOpenLogTab: true, logDock: "right", ...info?.config ?? {} } });
308
333
  } catch {
309
334
  }
310
335
  }
@@ -396,7 +421,7 @@ async function startTarget(project, target) {
396
421
  kind: target.kind,
397
422
  module: target.module
398
423
  });
399
- if (store.get().config.autoOpenLogTab !== false) openLogTab();
424
+ if (store.get().config.autoOpenLogTab !== false) openLogTab(store.get().config);
400
425
  } catch (error) {
401
426
  launchStore.set({ error: error instanceof Error ? error.message : String(error) });
402
427
  } finally {
@@ -837,7 +862,7 @@ function DetailTab() {
837
862
  ),
838
863
  h(
839
864
  "button",
840
- { type: "button", className: "mcdev-run", onClick: () => openLogTab() },
865
+ { type: "button", className: "mcdev-run", onClick: () => openLogTab(store.get().config) },
841
866
  "\u6253\u5F00\u65E5\u5FD7"
842
867
  )
843
868
  )
@@ -920,6 +945,66 @@ function row(key, value) {
920
945
 
921
946
  // src/client/LogTab.js
922
947
  var import_react5 = require("react");
948
+
949
+ // src/client/logFormat.js
950
+ var ANSI = /\u001b\[[0-9;]*[A-Za-z]/g;
951
+ function seg(text2, kind) {
952
+ return { text: text2, kind };
953
+ }
954
+ function levelKind(level) {
955
+ const upper = level.toUpperCase();
956
+ if (upper === "WARN" || upper === "WARNING") return "level-warn";
957
+ if (upper === "ERROR" || upper === "FATAL" || upper === "SEVERE") return "level-error";
958
+ if (upper === "DEBUG" || upper === "TRACE" || upper === "FINE" || upper === "FINER") return "level-debug";
959
+ return "level-info";
960
+ }
961
+ var LOG4J = /^\[(\d{2}:\d{2}:\d{2})\]\s+\[([^\]]+?)\/([A-Z]+)\]\s*(?:\[([^\]]*)\]:\s*|:\s*)?(.*)$/;
962
+ var PAPER = /^\[(\d{2}:\d{2}:\d{2}) ([A-Z]+)\]:\s*(.*)$/;
963
+ var BARE_LEVEL = /^(TRACE|DEBUG|INFO|WARN|WARNING|ERROR|FATAL)\s+(.*)$/;
964
+ var STACK = /^\s+(at |\.\.\. \d+ more)/;
965
+ function highlightLine(raw) {
966
+ const line = String(raw ?? "").replace(ANSI, "");
967
+ if (line === "") return [seg("", "plain")];
968
+ if (/^Caused by:|^\s+Suppressed:/.test(line)) return [seg(line, "fail")];
969
+ if (STACK.test(line)) return [seg(line, "stack")];
970
+ const log4j = LOG4J.exec(line);
971
+ if (log4j !== null) {
972
+ const [, time, thread, level, logger, message] = log4j;
973
+ const kind = levelKind(level);
974
+ const parts = [
975
+ seg(`[${time}] `, "time"),
976
+ seg(`[${thread}/`, "thread"),
977
+ seg(level, kind),
978
+ seg(logger === void 0 ? "]: " : "] ", "thread")
979
+ ];
980
+ if (logger !== void 0) parts.push(seg(`[${logger}]: `, "logger"));
981
+ parts.push(seg(message, kind === "level-error" ? "level-error" : "message"));
982
+ return parts;
983
+ }
984
+ const paper = PAPER.exec(line);
985
+ if (paper !== null) {
986
+ const [, time, level, message] = paper;
987
+ return [
988
+ seg(`[${time} `, "time"),
989
+ seg(level, levelKind(level)),
990
+ seg("]: ", "time"),
991
+ seg(message, levelKind(level) === "level-error" ? "level-error" : "message")
992
+ ];
993
+ }
994
+ const bare = BARE_LEVEL.exec(line);
995
+ if (bare !== null) {
996
+ const [, level, message] = bare;
997
+ return [seg(`${level} `, levelKind(level)), seg(message, levelKind(level) === "level-error" ? "level-error" : "message")];
998
+ }
999
+ if (/^> Task /.test(line) || /^> Configure project /.test(line)) return [seg(line, "task")];
1000
+ if (/^BUILD SUCCESSFUL/.test(line)) return [seg(line, "ok")];
1001
+ if (/^BUILD FAILED/.test(line) || /^FAILURE:/.test(line)) return [seg(line, "fail")];
1002
+ if (/^\* What went wrong:|^\* Where:|^\* Try:/.test(line)) return [seg(line, "level-warn")];
1003
+ if (/^(Reusing configuration cache|Deprecated Gradle features|You can use)/.test(line)) return [seg(line, "muted")];
1004
+ return [seg(line, "plain")];
1005
+ }
1006
+
1007
+ // src/client/LogTab.js
923
1008
  var lastLogId;
924
1009
  function LogTab() {
925
1010
  const launches = useLaunches(1500);
@@ -1029,7 +1114,20 @@ function LogTab() {
1029
1114
  ),
1030
1115
  h("div", { className: "mcdev-hint mcdev-code" }, log.diagnosis.line)
1031
1116
  ),
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),
1117
+ log?.error === void 0 ? log === void 0 && running ? h(Skeleton, { rows: 4, hint: "\u6B63\u5728\u8BFB\u53D6\u65E5\u5FD7\u2026" }) : h(
1118
+ "pre",
1119
+ { ref: body, className: "mcdev-logs mcdev-logs-full" },
1120
+ (log?.lines ?? []).length === 0 ? "\uFF08\u8FD8\u6CA1\u6709\u8F93\u51FA\uFF09" : (log?.lines ?? []).map(
1121
+ (line, index) => h(
1122
+ "span",
1123
+ { key: index, className: "mcdev-log-line" },
1124
+ highlightLine(line).map(
1125
+ (part, partIndex) => h("span", { key: partIndex, "data-kind": part.kind }, part.text)
1126
+ ),
1127
+ "\n"
1128
+ )
1129
+ )
1130
+ ) : h("div", { className: "mcdev-error", style: { marginTop: 8 } }, log.error),
1033
1131
  h(
1034
1132
  "div",
1035
1133
  { style: { marginTop: 8 } },
@@ -1302,6 +1400,20 @@ var CSS = `
1302
1400
  .mcdev-diagnosis-hint { color: var(--dsw-alias-state-error-primary); font: var(--dsw-font-xxs-strong-12); }
1303
1401
  .mcdev-logs-full { flex: 1 1 auto; min-height: 0; max-height: none; margin: 0; }
1304
1402
 
1403
+ /* \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 */
1404
+ .mcdev-log-line { display: inline; }
1405
+ .mcdev-log-line [data-kind="time"],
1406
+ .mcdev-log-line [data-kind="thread"],
1407
+ .mcdev-log-line [data-kind="logger"] { color: var(--dsw-alias-label-tertiary); }
1408
+ .mcdev-log-line [data-kind="muted"] { color: var(--dsw-alias-label-tertiary); }
1409
+ .mcdev-log-line [data-kind="level-warn"] { color: var(--dsw-alias-state-warn-primary); }
1410
+ .mcdev-log-line [data-kind="level-error"] { color: var(--dsw-alias-state-error-primary); }
1411
+ .mcdev-log-line [data-kind="level-debug"] { color: var(--dsw-alias-label-tertiary); }
1412
+ .mcdev-log-line [data-kind="task"] { color: var(--dsw-alias-label-secondary); }
1413
+ .mcdev-log-line [data-kind="ok"] { color: var(--dsw-alias-state-success-primary); }
1414
+ .mcdev-log-line [data-kind="fail"] { color: var(--dsw-alias-state-error-primary); font: var(--dsw-font-xxxs-strong-11); }
1415
+ .mcdev-log-line [data-kind="stack"] { color: var(--dsw-alias-label-secondary); }
1416
+
1305
1417
  .mcdev-section { margin-top: 12px; }
1306
1418
  .mcdev-section > h4 {
1307
1419
  margin: 0 0 4px;
@@ -1422,7 +1534,7 @@ function registerBetterSidebarTabs(ctx) {
1422
1534
  try {
1423
1535
  disposers.push(
1424
1536
  sidebar.registerTab({
1425
- id: "minecraft-dev-starter:project",
1537
+ id: BETTER_DETAIL_ID,
1426
1538
  title: PANEL_TITLE,
1427
1539
  icon: (size) => h(IconCube, { size }),
1428
1540
  component: () => h(DetailTab)
@@ -1430,7 +1542,7 @@ function registerBetterSidebarTabs(ctx) {
1430
1542
  );
1431
1543
  disposers.push(
1432
1544
  sidebar.registerTab({
1433
- id: "minecraft-dev-starter:logs",
1545
+ id: BETTER_LOG_ID,
1434
1546
  title: "\u8FD0\u884C\u65E5\u5FD7",
1435
1547
  icon: (size) => h(IconTerminal, { size }),
1436
1548
  component: () => h(LogTab)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-dev-starter",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -17,6 +17,7 @@ import {
17
17
  selectedProject,
18
18
  startTarget,
19
19
  stopLaunch,
20
+ store,
20
21
  useLaunches,
21
22
  useStoreState,
22
23
  } from './state.js'
@@ -122,7 +123,7 @@ export function DetailTab() {
122
123
  ),
123
124
  h(
124
125
  'button',
125
- { type: 'button', className: 'mcdev-run', onClick: () => openLogTab() },
126
+ { type: 'button', className: 'mcdev-run', onClick: () => openLogTab(store.get().config) },
126
127
  '打开日志',
127
128
  ),
128
129
  ),
@@ -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,20 @@
1
+ /**
2
+ * 日志标签的落点选择。
3
+ *
4
+ * 右列属于 DSH 原生右侧栏;底部工作台是 dsh-better-sidebar 提供的。
5
+ * 未安装 better-sidebar 时只能落在右侧栏,配置值在这种情况下被忽略。
6
+ * @module minecraft-dev-starter/client/dock
7
+ */
8
+
9
+ export const LOG_DOCK_RIGHT = 'right'
10
+ export const LOG_DOCK_BOTTOM = 'bottom'
11
+
12
+ /**
13
+ * @param {{ logDock?: string } | undefined} config
14
+ * @param {boolean} hasBetterSidebar
15
+ * @returns {'right' | 'bottom'}
16
+ */
17
+ export function chooseLogDock(config, hasBetterSidebar) {
18
+ if (hasBetterSidebar !== true) return LOG_DOCK_RIGHT
19
+ return config?.logDock === LOG_DOCK_BOTTOM ? LOG_DOCK_BOTTOM : LOG_DOCK_RIGHT
20
+ }
@@ -17,7 +17,7 @@ import { LogTab } from './LogTab.js'
17
17
  import { report } from './debug.js'
18
18
  import { h } from './h.js'
19
19
  import { IconCube, IconTerminal } from './icons.js'
20
- import { DETAIL_KIND, LOG_KIND, runtime } from './runtime.js'
20
+ import { BETTER_DETAIL_ID, BETTER_LOG_ID, DETAIL_KIND, LOG_KIND, runtime } from './runtime.js'
21
21
  import { ensureStyles } from './styles.js'
22
22
 
23
23
  export const name = 'mcdev-client'
@@ -99,7 +99,7 @@ function registerBetterSidebarTabs(ctx) {
99
99
  try {
100
100
  disposers.push(
101
101
  sidebar.registerTab({
102
- id: 'minecraft-dev-starter:project',
102
+ id: BETTER_DETAIL_ID,
103
103
  title: PANEL_TITLE,
104
104
  icon: (size) => h(IconCube, { size }),
105
105
  component: () => h(DetailTab),
@@ -107,7 +107,7 @@ function registerBetterSidebarTabs(ctx) {
107
107
  )
108
108
  disposers.push(
109
109
  sidebar.registerTab({
110
- id: 'minecraft-dev-starter:logs',
110
+ id: BETTER_LOG_ID,
111
111
  title: '运行日志',
112
112
  icon: (size) => h(IconTerminal, { size }),
113
113
  component: () => h(LogTab),
@@ -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
+ }
@@ -6,12 +6,18 @@
6
6
  * @module minecraft-dev-starter/client/runtime
7
7
  */
8
8
 
9
- /** 右侧栏「MC 工程」详情标签的 kind。 */
9
+ import { chooseLogDock, LOG_DOCK_BOTTOM } from './dock.js'
10
+
11
+ /** 右侧栏「Mod Dev」详情标签的 kind。 */
10
12
  export const DETAIL_KIND = 'mcdev.detail'
11
13
 
12
14
  /** 右侧栏「运行日志」标签的 kind。 */
13
15
  export const LOG_KIND = 'mcdev.logs'
14
16
 
17
+ /** 向 dsh-better-sidebar 注册时用的 tab id;打开底部工作台需要同一个 id。 */
18
+ export const BETTER_DETAIL_ID = 'minecraft-dev-starter:project'
19
+ export const BETTER_LOG_ID = 'minecraft-dev-starter:logs'
20
+
15
21
  /** @type {{ ctx?: any }} */
16
22
  export const runtime = {}
17
23
 
@@ -30,12 +36,39 @@ function open(kind) {
30
36
  }
31
37
  }
32
38
 
39
+ /**
40
+ * 取可选的 better-sidebar 服务;未安装时返回 undefined(不抛错)。
41
+ * @returns {any}
42
+ */
43
+ export function betterSidebar() {
44
+ try {
45
+ return runtime.ctx?.get?.('betterSidebar', false)
46
+ } catch {
47
+ return undefined
48
+ }
49
+ }
50
+
33
51
  /** 打开工程详情标签(openTab 会顺带展开面板)。 */
34
52
  export function openDetailTab() {
35
53
  return open(DETAIL_KIND)
36
54
  }
37
55
 
38
- /** 打开运行日志标签。 */
39
- export function openLogTab() {
56
+ /**
57
+ * 打开运行日志标签。
58
+ *
59
+ * 落点由配置 `logDock` 决定:`bottom` 且装了 better-sidebar 时落到底部工作台,
60
+ * 其余情况一律落原生右侧栏。配置由调用方传入,避免 runtime ←→ state 循环依赖。
61
+ * @param {{ logDock?: string } | undefined} [config]
62
+ */
63
+ export function openLogTab(config) {
64
+ const service = betterSidebar()
65
+ if (chooseLogDock(config, service !== undefined) === LOG_DOCK_BOTTOM) {
66
+ try {
67
+ service.openTab({ type: BETTER_LOG_ID, title: '运行日志', target: 'bottom' })
68
+ return true
69
+ } catch {
70
+ /* 落底部失败就退回右侧栏,不吞掉日志入口 */
71
+ }
72
+ }
40
73
  return open(LOG_KIND)
41
74
  }
@@ -25,8 +25,8 @@ export const store = createStore({
25
25
  flyoutOpen: false,
26
26
  /** @type {Record<string, string>} 每个工程记住的目标 key */
27
27
  selection: readSelection(),
28
- /** @type {{ autoOpenLogTab?: boolean }} 宿主下发的部署配置 */
29
- config: { autoOpenLogTab: true },
28
+ /** @type {{ autoOpenLogTab?: boolean, logDock?: 'right' | 'bottom' }} 宿主下发的部署配置 */
29
+ config: { autoOpenLogTab: true, logDock: 'right' },
30
30
  /** @type {string | undefined} 当前会话的工作区(由胶囊从会话上下文注入) */
31
31
  workspaceRoot: undefined,
32
32
  })
@@ -37,7 +37,7 @@ export const store = createStore({
37
37
  export async function loadStateInfo() {
38
38
  try {
39
39
  const info = await call('state.info', {})
40
- store.set({ config: { autoOpenLogTab: true, ...(info?.config ?? {}) } })
40
+ store.set({ config: { autoOpenLogTab: true, logDock: 'right', ...(info?.config ?? {}) } })
41
41
  } catch {
42
42
  /* 拉不到就用默认值 */
43
43
  }
@@ -172,7 +172,7 @@ export async function startTarget(project, target) {
172
172
  module: target.module,
173
173
  })
174
174
  // 启动成功就把日志摆到人眼前(部署配置可关)
175
- if (store.get().config.autoOpenLogTab !== false) openLogTab()
175
+ if (store.get().config.autoOpenLogTab !== false) openLogTab(store.get().config)
176
176
  } catch (error) {
177
177
  launchStore.set({ error: error instanceof Error ? error.message : String(error) })
178
178
  } finally {
@@ -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;
package/src/index.js CHANGED
@@ -117,6 +117,7 @@ function resolveWorkspaceRoot(ctx) {
117
117
  /** 部署配置默认值;patch 行的 `config` 会覆盖同名项。 */
118
118
  const DEFAULTS = {
119
119
  autoOpenLogTab: true,
120
+ logDock: 'right',
120
121
  readyTimeoutMs: 90_000,
121
122
  logRingLines: 2_000,
122
123
  }
@@ -131,7 +132,10 @@ export function apply(ctx, config = {}) {
131
132
  version: VERSION,
132
133
  resolveRoot: () => resolveWorkspaceRoot(ctx),
133
134
  resolveRoots: () => sessionCwds(ctx),
134
- config: { autoOpenLogTab: settings.autoOpenLogTab !== false },
135
+ config: {
136
+ autoOpenLogTab: settings.autoOpenLogTab !== false,
137
+ logDock: settings.logDock === 'bottom' ? 'bottom' : 'right',
138
+ },
135
139
  launchService: createLaunchService({
136
140
  readyTimeoutMs: Number(settings.readyTimeoutMs) || DEFAULTS.readyTimeoutMs,
137
141
  maxLogLines: Number(settings.logRingLines) || DEFAULTS.logRingLines,