minecraft-dev-starter 0.0.0-stage → 0.1.1

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/src/tools.js ADDED
@@ -0,0 +1,291 @@
1
+ /**
2
+ * 模型侧工具(M5):面板能做的事,AI 也该能做,而且是同一套服务。
3
+ *
4
+ * 四个工具都薄封装在 `/mcdev/api` 的方法表上,不复制业务逻辑:
5
+ * mc_projects / mc_launch / mc_launch_logs / mc_launch_stop
6
+ *
7
+ * 依赖 `@deepseek-ai/dsh-tools` 的 `defineTool`(与桌面运行时同版本 0.2.1-rc.3)。
8
+ * @module minecraft-dev-starter/tools
9
+ */
10
+
11
+ import { defineTool } from '@deepseek-ai/dsh-tools'
12
+
13
+ /**
14
+ * 工具的工作区根:调用参数优先,其次取**调用它的那个会话**的 cwd。
15
+ *
16
+ * 宿主里可能有多个会话、各自的 cwd 不同;用"最近一个会话"当默认会把
17
+ * 别的会话的工程当成当前工作区。
18
+ * @param {{ root?: string }} args
19
+ * @param {{ agent?: any } | undefined} exec
20
+ * @returns {string | undefined}
21
+ */
22
+ function agentRoot(args, exec) {
23
+ if (typeof args.root === 'string' && args.root !== '') return args.root
24
+ const cwd = exec?.agent?.session?.header?.cwd
25
+ return typeof cwd === 'string' && cwd !== '' ? cwd : undefined
26
+ }
27
+
28
+ /**
29
+ * 等实例进入 running 或结束。
30
+ * @param {{ 'launch.list': (payload?: unknown) => Promise<any> }} api
31
+ * @param {string} launchId
32
+ * @param {number} timeoutMs
33
+ * @param {AbortSignal | undefined} signal
34
+ * @returns {Promise<Record<string, any> | undefined>}
35
+ */
36
+ async function waitForSettle(api, launchId, timeoutMs, signal) {
37
+ const started = Date.now()
38
+ while (Date.now() - started < timeoutMs) {
39
+ if (signal?.aborted === true) return undefined
40
+ const list = await api['launch.list']({})
41
+ const entry = list.find((item) => item.id === launchId)
42
+ if (entry === undefined) return undefined
43
+ if (entry.status === 'running' || entry.status === 'exited' || entry.status === 'unknown') return entry
44
+ await new Promise((resolve) => setTimeout(resolve, 400))
45
+ }
46
+ return undefined
47
+ }
48
+
49
+ /**
50
+ * mc_launch 的输出值:只放存在的字段(`pid` 可能是 null),保证无损 JSON。
51
+ * @param {Record<string, any>} record
52
+ * @param {boolean} timedOut
53
+ */
54
+ function launchValue(record, timedOut) {
55
+ /** @type {Record<string, any>} */
56
+ const value = {
57
+ id: record.id,
58
+ status: record.status,
59
+ taskPath: record.taskPath,
60
+ logPath: record.logPath,
61
+ timedOut,
62
+ }
63
+ if (typeof record.pid === 'number') value.pid = record.pid
64
+ return value
65
+ }
66
+
67
+ /**
68
+ * 注意:这里渲染的是工具的**输出值**(紧凑形状,字段是 `targets`),
69
+ * 不是工程画像(字段是 `runTargets`)。早期版本读错字段,导致"0 个目标"。
70
+ * @param {{ root: string, projects: Array<Record<string, any>> }} value
71
+ * @returns {string}
72
+ */
73
+ function renderProjects(value) {
74
+ const projects = /** @type {Array<Record<string, any>>} */ (value.projects ?? [])
75
+ if (projects.length === 0) return `工作区 ${value.root} 里没有识别到 Minecraft 工程。`
76
+ const lines = [`工作区 ${value.root}:${projects.length} 个工程`]
77
+ for (const project of projects) {
78
+ const targets = /** @type {string[]} */ (project.targets ?? [])
79
+ lines.push(
80
+ `- ${project.name} · ${project.loader} ${project.minecraftVersion ?? '?'} · ${targets.length} 个目标:${targets.length === 0 ? '无可运行目标' : targets.join(', ')}`,
81
+ )
82
+ }
83
+ return lines.join('\n')
84
+ }
85
+
86
+ /**
87
+ * 注册 M5 的四个工具。
88
+ * @param {any} ctx
89
+ * @param {{ api: Record<string, any> }} deps
90
+ */
91
+ export function applyTools(ctx, deps) {
92
+ const api = deps.api
93
+
94
+ ctx.tools.register(
95
+ defineTool({
96
+ name: 'mc_projects',
97
+ description:
98
+ '列出当前工作区里识别到的 Minecraft 工程(Fabric/Forge/NeoForge/Paper/Architectury)以及每个工程可运行的目标(runClient、runServer…)。'
99
+ + ' 只读。要构建用 mc_gradle,要真正起客户端/服务端用 mc_launch。',
100
+ parameters: {
101
+ root: { type: 'string', description: '工作区目录;省略则用当前会话的工作区' },
102
+ refresh: { type: 'boolean', description: '跳过 5 秒缓存重新扫描' },
103
+ },
104
+ output: {
105
+ schema: {
106
+ type: 'object',
107
+ additionalProperties: false,
108
+ properties: {
109
+ root: { type: 'string', required: true },
110
+ projects: {
111
+ type: 'array',
112
+ required: true,
113
+ items: {
114
+ type: 'object',
115
+ additionalProperties: true,
116
+ properties: {
117
+ name: { type: 'string' },
118
+ root: { type: 'string' },
119
+ loader: { type: 'string' },
120
+ minecraftVersion: { type: 'string' },
121
+ targets: { type: 'array', items: { type: 'string' } },
122
+ },
123
+ },
124
+ },
125
+ truncated: { type: 'boolean', required: true },
126
+ },
127
+ },
128
+ render: (_args, value) => [{ type: 'text', text: renderProjects(value) }],
129
+ },
130
+ async execute(args, exec) {
131
+ const scan = await api['projects.scan']({ root: agentRoot(args, exec), refresh: args.refresh === true })
132
+ return {
133
+ root: scan.root,
134
+ truncated: scan.truncated === true,
135
+ projects: (scan.projects ?? []).map((project) => ({
136
+ name: project.name,
137
+ root: project.root,
138
+ loader: project.loader,
139
+ minecraftVersion: project.minecraftVersion ?? '',
140
+ targets: (project.runTargets ?? []).map((target) => `${target.taskPath}${target.guessed === true ? ' (推测)' : ''}`),
141
+ })),
142
+ }
143
+ },
144
+ }),
145
+ )
146
+
147
+ ctx.tools.register(
148
+ defineTool({
149
+ name: 'mc_launch',
150
+ description:
151
+ '在后台启动一个 Minecraft 开发工程的运行目标(默认等它进入运行态再返回)。'
152
+ + ' 同一个工程同时只能有一个实例;任务路径必须是 mc_projects 列出的那些(形如 runClient 或 :fabric:runClient)。'
153
+ + ' 启动后可用 mc_launch_logs 看日志、mc_launch_stop 停止。',
154
+ parameters: {
155
+ root: { type: 'string', description: '工程根目录;省略则用当前工作区' },
156
+ taskPath: { type: 'string', required: true, description: 'Gradle 任务路径,例如 runClient、:fabric:runClient' },
157
+ wait: { type: 'string', enum: ['ready', 'detach'], description: 'ready(默认)等到运行态或结束;detach 立刻返回' },
158
+ timeoutMs: { type: 'number', description: '等就绪的上限,默认 60000' },
159
+ },
160
+ output: {
161
+ schema: {
162
+ type: 'object',
163
+ additionalProperties: false,
164
+ properties: {
165
+ id: { type: 'string', required: true },
166
+ status: { type: 'string', required: true },
167
+ taskPath: { type: 'string', required: true },
168
+ pid: { type: 'integer' },
169
+ logPath: { type: 'string', required: true },
170
+ timedOut: { type: 'boolean', required: true },
171
+ },
172
+ },
173
+ render: (_args, value) => [
174
+ {
175
+ type: 'text',
176
+ text: value.timedOut
177
+ ? `已启动 ${value.taskPath}(${value.id}),但在超时前没看到就绪标记;用 mc_launch_logs 看进展,可能还在构建。`
178
+ : `启动 ${value.taskPath}:状态 ${value.status},实例 ${value.id}${value.pid === undefined ? '' : `,pid ${value.pid}`}。日志在 ${value.logPath}。`,
179
+ },
180
+ ],
181
+ },
182
+ async execute(args, exec) {
183
+ const record = await api['launch.start']({ root: agentRoot(args, exec), taskPath: args.taskPath })
184
+ if (args.wait === 'detach') {
185
+ return launchValue(record, false)
186
+ }
187
+ const timeoutMs = Number.isFinite(args.timeoutMs) ? Number(args.timeoutMs) : 60_000
188
+ const settled = await waitForSettle(api, record.id, timeoutMs, exec.signal)
189
+ return launchValue(settled ?? record, settled === undefined)
190
+ },
191
+ }),
192
+ )
193
+
194
+ ctx.tools.register(
195
+ defineTool({
196
+ name: 'mc_launch_logs',
197
+ description: '读某个运行实例的日志(默认当前工程正在跑的那个)。异常退出时附带一句归因。',
198
+ parameters: {
199
+ id: { type: 'string', description: '实例 id(launch-N);省略则用当前工程活跃实例' },
200
+ root: { type: 'string', description: '工程根目录;配合 id 省略时使用' },
201
+ offset: { type: 'number', description: '起始行号,默认 0' },
202
+ limit: { type: 'number', description: '最多返回多少行,默认 200' },
203
+ },
204
+ output: {
205
+ schema: {
206
+ type: 'object',
207
+ additionalProperties: false,
208
+ properties: {
209
+ launchId: { type: 'string', required: true },
210
+ status: { type: 'string', required: true },
211
+ outcome: { type: 'string' },
212
+ exitCode: { type: 'integer' },
213
+ lines: { type: 'array', required: true, items: { type: 'string' } },
214
+ total: { type: 'integer', required: true },
215
+ diagnosis: { type: 'string' },
216
+ },
217
+ },
218
+ render: (_args, value) => [
219
+ {
220
+ type: 'text',
221
+ text:
222
+ `${value.launchId} · ${value.status}${value.outcome === undefined ? '' : `/${value.outcome}`}`
223
+ + `${value.diagnosis === undefined ? '' : `\n归因:${value.diagnosis}`}`
224
+ + `\n${value.lines.slice(-40).join('\n')}`,
225
+ },
226
+ ],
227
+ },
228
+ async execute(args, exec) {
229
+ let id = args.id
230
+ if (id === undefined || id === '') {
231
+ const current = await api['launch.current']({ root: agentRoot(args, exec) })
232
+ if (current === null || current === undefined) throw new Error('当前工程没有运行中的实例;请传 id')
233
+ id = current.id
234
+ }
235
+ const result = await api['launch.logs']({
236
+ id,
237
+ offset: Number.isFinite(args.offset) ? Number(args.offset) : 0,
238
+ limit: Number.isFinite(args.limit) ? Number(args.limit) : 200,
239
+ })
240
+ // 只放存在字段:工具结果必须是"无损 JSON",带 undefined 会被注册器整条拒掉
241
+ /** @type {Record<string, any>} */
242
+ const value = {
243
+ launchId: result.launchId,
244
+ status: result.status,
245
+ lines: result.lines ?? [],
246
+ total: result.total ?? 0,
247
+ }
248
+ if (result.outcome !== null && result.outcome !== undefined) value.outcome = result.outcome
249
+ if (typeof result.exitCode === 'number') value.exitCode = result.exitCode
250
+ if (result.diagnosis?.hint !== undefined) value.diagnosis = result.diagnosis.hint
251
+ return value
252
+ },
253
+ }),
254
+ )
255
+
256
+ ctx.tools.register(
257
+ defineTool({
258
+ name: 'mc_launch_stop',
259
+ description: '停止一个运行实例(连同它拉起的整个进程树)。省略 id 时停当前工程的活跃实例。',
260
+ parameters: {
261
+ id: { type: 'string', description: '实例 id;省略则用当前工程活跃实例' },
262
+ root: { type: 'string', description: '工程根目录;配合 id 省略时使用' },
263
+ },
264
+ output: {
265
+ schema: {
266
+ type: 'object',
267
+ additionalProperties: false,
268
+ properties: {
269
+ id: { type: 'string', required: true },
270
+ status: { type: 'string', required: true },
271
+ outcome: { type: 'string' },
272
+ },
273
+ },
274
+ render: (_args, value) => [{ type: 'text', text: `已停止 ${value.id}:${value.status}${value.outcome === undefined ? '' : `/${value.outcome}`}` }],
275
+ },
276
+ async execute(args, exec) {
277
+ let id = args.id
278
+ if (id === undefined || id === '') {
279
+ const current = await api['launch.current']({ root: agentRoot(args, exec) })
280
+ if (current === null || current === undefined) throw new Error('当前工程没有运行中的实例;请传 id')
281
+ id = current.id
282
+ }
283
+ const record = await api['launch.stop']({ id })
284
+ /** @type {Record<string, any>} */
285
+ const value = { id: record.id, status: record.status }
286
+ if (record.outcome !== null && record.outcome !== undefined) value.outcome = record.outcome
287
+ return value
288
+ },
289
+ }),
290
+ )
291
+ }