mioku-plugin-mc 2.1.0 → 3.0.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/config.md +157 -3
- package/index.ts +61 -9
- package/package.json +8 -3
- package/play/actions/registry.ts +196 -0
- package/play/ai/context-builder.ts +29 -0
- package/play/ai/debug-log.ts +135 -0
- package/play/ai/main-loop.ts +306 -0
- package/play/ai/main-tools.ts +230 -0
- package/play/ai/prompt.ts +207 -0
- package/play/ai/work-subroutine.ts +487 -0
- package/play/behavior/base-behavior.ts +82 -0
- package/play/behavior/catalog/approach-player.ts +65 -0
- package/play/behavior/catalog/defend.ts +68 -0
- package/play/behavior/catalog/explore.ts +72 -0
- package/play/behavior/catalog/factory.ts +26 -0
- package/play/behavior/catalog/farm-mobs.ts +43 -0
- package/play/behavior/catalog/follow.ts +80 -0
- package/play/behavior/catalog/gather.ts +135 -0
- package/play/behavior/catalog/idle.ts +130 -0
- package/play/behavior/catalog/seek-shelter.ts +85 -0
- package/play/behavior/engine.ts +243 -0
- package/play/behavior/survival/auto-eat.ts +46 -0
- package/play/behavior/survival/escape-lava.ts +44 -0
- package/play/behavior/survival/escape-water.ts +35 -0
- package/play/behavior/survival/flee-creeper.ts +76 -0
- package/play/behavior/survival/mlg-fall.ts +56 -0
- package/play/bot/bot-controller.ts +276 -0
- package/play/bot/play-bus.ts +48 -0
- package/play/combat/combat.ts +225 -0
- package/play/combat/index.ts +1 -0
- package/play/config.ts +39 -0
- package/play/context.ts +26 -0
- package/play/debug/README.md +74 -0
- package/play/debug/commands.ts +289 -0
- package/play/index.ts +247 -0
- package/play/mineflayer-shims.d.ts +22 -0
- package/play/missions/bundles/approach-player.ts +26 -0
- package/play/missions/bundles/explore.ts +24 -0
- package/play/missions/bundles/farm-mobs.ts +24 -0
- package/play/missions/bundles/follow-player.ts +40 -0
- package/play/missions/bundles/gather-resource.ts +37 -0
- package/play/missions/bundles/idle-wander.ts +24 -0
- package/play/missions/bundles/seek-shelter.ts +18 -0
- package/play/missions/mission-controller.ts +296 -0
- package/play/missions/registry.ts +107 -0
- package/play/path-engine/astar.ts +514 -0
- package/play/path-engine/goals.ts +84 -0
- package/play/path-engine/index.ts +4 -0
- package/play/path-engine/movements.ts +67 -0
- package/play/path-engine/path-engine.ts +540 -0
- package/play/session.ts +486 -0
- package/play/state/cooldowns.ts +40 -0
- package/play/state/event-journal.ts +72 -0
- package/play/state/memory-bus.ts +115 -0
- package/play/state/mode.ts +57 -0
- package/play/state/sensors/entity-scanner.ts +275 -0
- package/play/state/snapshot.ts +360 -0
- package/play/types.ts +284 -0
- package/play/util/async.ts +11 -0
- package/play/util/endpoint.ts +38 -0
- package/play/util/entities.ts +135 -0
- package/play/util/inventory.ts +75 -0
- package/skills/mc.ts +58 -0
package/config.md
CHANGED
|
@@ -75,12 +75,12 @@ fields:
|
|
|
75
75
|
|
|
76
76
|
- key: command_user
|
|
77
77
|
label: 命令白名单
|
|
78
|
-
type:
|
|
78
|
+
type: textarea
|
|
79
79
|
description: 允许执行 RCON 命令的 QQ 号列表,每行一个
|
|
80
80
|
|
|
81
81
|
- key: rcon_command_whitelist
|
|
82
82
|
label: RCON 命令白名单
|
|
83
|
-
type:
|
|
83
|
+
type: textarea
|
|
84
84
|
description: 允许执行的 RCON 命令列表,每行一个
|
|
85
85
|
|
|
86
86
|
- key: sync_enabled
|
|
@@ -125,6 +125,145 @@ fields:
|
|
|
125
125
|
label: RCON 密码
|
|
126
126
|
type: secret
|
|
127
127
|
description: RCON 密码
|
|
128
|
+
|
|
129
|
+
- key: play.servers
|
|
130
|
+
label: 游玩服务器列表
|
|
131
|
+
type: array
|
|
132
|
+
description: bot 可加入游玩的 Minecraft 服务器(原版/leaves)
|
|
133
|
+
itemFields:
|
|
134
|
+
- key: id
|
|
135
|
+
label: 服务器 ID
|
|
136
|
+
type: text
|
|
137
|
+
description: 唯一标识,AI 工具按此选择服务器
|
|
138
|
+
placeholder: survival
|
|
139
|
+
- key: name
|
|
140
|
+
label: 显示名称
|
|
141
|
+
type: text
|
|
142
|
+
description: 服务器显示名
|
|
143
|
+
placeholder: 生存服
|
|
144
|
+
- key: host
|
|
145
|
+
label: 地址
|
|
146
|
+
type: text
|
|
147
|
+
description: 服务器地址。可写 host[:port](如 play.example.com:25565),省略端口时会自动尝试 _minecraft._tcp.<host> 的 SRV 解析,失败则默认 25565
|
|
148
|
+
placeholder: play.example.com
|
|
149
|
+
- key: version
|
|
150
|
+
label: 版本
|
|
151
|
+
type: text
|
|
152
|
+
description: Minecraft 版本,留空自动协商
|
|
153
|
+
placeholder: "1.20.4"
|
|
154
|
+
- key: username
|
|
155
|
+
label: 假人名称
|
|
156
|
+
type: text
|
|
157
|
+
description: bot 加入服务器使用的名字
|
|
158
|
+
placeholder: MikuBot
|
|
159
|
+
- key: auth
|
|
160
|
+
label: 认证方式
|
|
161
|
+
type: text
|
|
162
|
+
description: offline 或 microsoft,默认 offline
|
|
163
|
+
placeholder: offline
|
|
164
|
+
- key: password
|
|
165
|
+
label: 认证密码
|
|
166
|
+
type: secret
|
|
167
|
+
description: microsoft 认证时使用
|
|
168
|
+
- key: maxPlayMs
|
|
169
|
+
label: 最长游玩时长(ms)
|
|
170
|
+
type: number
|
|
171
|
+
description: 单次游玩最长时长,到点自动下线
|
|
172
|
+
placeholder: 1800000
|
|
173
|
+
|
|
174
|
+
- key: joinCommands
|
|
175
|
+
label: 加入后自动执行的命令
|
|
176
|
+
type: textarea
|
|
177
|
+
description: bot 第一次进入该服务器后自动发送的命令(每行一条,如 /login xxx),仅在首次 spawn 时执行,重生不重复
|
|
178
|
+
|
|
179
|
+
- key: allowedCommands
|
|
180
|
+
label: AI 命令白名单
|
|
181
|
+
type: textarea
|
|
182
|
+
description: Working AI 可发送的斜杠命令前缀,每行一条;未列出的命令会被拒绝
|
|
183
|
+
|
|
184
|
+
- key: play.groups
|
|
185
|
+
label: 群聊绑定
|
|
186
|
+
type: array
|
|
187
|
+
description: 配置哪些群允许触发 bot 游玩及可选服务器
|
|
188
|
+
itemFields:
|
|
189
|
+
- key: groupId
|
|
190
|
+
label: 群号
|
|
191
|
+
type: text
|
|
192
|
+
description: QQ 群号
|
|
193
|
+
placeholder: "518680610"
|
|
194
|
+
- key: botSelfId
|
|
195
|
+
label: 机器人账号
|
|
196
|
+
type: text
|
|
197
|
+
description: 该群使用的 bot QQ 号
|
|
198
|
+
- key: allowedServerIds
|
|
199
|
+
label: 允许的服务器
|
|
200
|
+
type: textarea
|
|
201
|
+
description: 允许该群触发的服务器 ID,每行一个
|
|
202
|
+
|
|
203
|
+
- key: play.toolPermission
|
|
204
|
+
label: 工具权限
|
|
205
|
+
type: text
|
|
206
|
+
description: 谁可触发 bot 进出服(owner/admin/member,需重启生效)
|
|
207
|
+
placeholder: admin
|
|
208
|
+
- key: play.mainChatDebounceMs
|
|
209
|
+
label: 主 AI 防抖(ms)
|
|
210
|
+
type: number
|
|
211
|
+
description: 同一玩家连续触发主 AI 的最小间隔(仅 @bot/名字时才生效)
|
|
212
|
+
placeholder: 1000
|
|
213
|
+
- key: play.mainConversationFocusMs
|
|
214
|
+
label: 游戏连续对话窗口(ms)
|
|
215
|
+
type: number
|
|
216
|
+
description: 玩家明确呼叫 bot 后,后续消息可继续触发主 AI 的时间窗口
|
|
217
|
+
placeholder: 120000
|
|
218
|
+
- key: play.workTerminationCheckMs
|
|
219
|
+
label: 工作终止检查间隔(ms)
|
|
220
|
+
type: number
|
|
221
|
+
description: session watchdog 检查工作目标终止条件的频率
|
|
222
|
+
placeholder: 1000
|
|
223
|
+
- key: play.workStaleMs
|
|
224
|
+
label: 工作停滞超时(ms)
|
|
225
|
+
type: number
|
|
226
|
+
description: 工作目标在该时长内无进展时,自动拉起 work agent 重新评估
|
|
227
|
+
placeholder: 600000
|
|
228
|
+
- key: play.workGoalTimeoutMs
|
|
229
|
+
label: 工作目标总超时(ms)
|
|
230
|
+
type: number
|
|
231
|
+
description: 单个工作目标最长允许存活的时间,到点自动标记失败
|
|
232
|
+
placeholder: 1800000
|
|
233
|
+
- key: play.maxMissionMs
|
|
234
|
+
label: Mission 最大时长(ms)
|
|
235
|
+
type: number
|
|
236
|
+
description: 单个 mission 最大运行时间,到点强制结束(防 isFinished 缺失或抛错导致永久占用)
|
|
237
|
+
placeholder: 1800000
|
|
238
|
+
- key: play.behaviorTickIntervalMs
|
|
239
|
+
label: 行为引擎 tick(ms)
|
|
240
|
+
type: number
|
|
241
|
+
description: 行为引擎 tick 间隔
|
|
242
|
+
placeholder: 200
|
|
243
|
+
- key: play.maxPlayBudgetWarnRatio
|
|
244
|
+
label: 预算提醒比例
|
|
245
|
+
type: number
|
|
246
|
+
description: 到该比例时提醒主模型收尾
|
|
247
|
+
placeholder: 0.85
|
|
248
|
+
- key: play.goodbyeTimeoutMs
|
|
249
|
+
label: 告别超时(ms)
|
|
250
|
+
type: number
|
|
251
|
+
description: 生成告别语的超时时间
|
|
252
|
+
placeholder: 8000
|
|
253
|
+
- key: play.qqSendPerMinute
|
|
254
|
+
label: QQ 每分钟上限
|
|
255
|
+
type: number
|
|
256
|
+
description: bot 向 QQ 群发送消息的每分钟上限
|
|
257
|
+
placeholder: 3
|
|
258
|
+
- key: play.gameChatMinIntervalMs
|
|
259
|
+
label: 游戏发言间隔(ms)
|
|
260
|
+
type: number
|
|
261
|
+
description: bot 游戏内连续发言最小间隔
|
|
262
|
+
placeholder: 1500
|
|
263
|
+
- key: play.debug.enabled
|
|
264
|
+
label: 调试模式
|
|
265
|
+
type: switch
|
|
266
|
+
description: 开启后主人可用 /join /say /motion 等命令手动测试行为(不启动 AI 循环),详见 play/debug/README.md
|
|
128
267
|
---
|
|
129
268
|
|
|
130
269
|
```mioku-fields
|
|
@@ -136,4 +275,19 @@ keys:
|
|
|
136
275
|
- base.reverse_ws_path
|
|
137
276
|
- base.reverse_ws_password
|
|
138
277
|
- base.servers
|
|
139
|
-
|
|
278
|
+
- play.servers
|
|
279
|
+
- play.groups
|
|
280
|
+
- play.toolPermission
|
|
281
|
+
- play.mainChatDebounceMs
|
|
282
|
+
- play.mainConversationFocusMs
|
|
283
|
+
- play.workTerminationCheckMs
|
|
284
|
+
- play.workStaleMs
|
|
285
|
+
- play.workGoalTimeoutMs
|
|
286
|
+
- play.maxMissionMs
|
|
287
|
+
- play.behaviorTickIntervalMs
|
|
288
|
+
- play.maxPlayBudgetWarnRatio
|
|
289
|
+
- play.goodbyeTimeoutMs
|
|
290
|
+
- play.qqSendPerMinute
|
|
291
|
+
- play.gameChatMinIntervalMs
|
|
292
|
+
- play.debug.enabled
|
|
293
|
+
```
|
package/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { definePlugin, type MiokiContext } from "mioki";
|
|
2
|
-
import
|
|
2
|
+
import { getService, Services } from "mioku";
|
|
3
3
|
import { createConfigHandler } from "./utils/config-handler";
|
|
4
|
+
import { createPlayConfigHandler } from "./play/config";
|
|
5
|
+
import { createPlayManager } from "./play";
|
|
6
|
+
import { createMcSkill } from "./skills/mc";
|
|
7
|
+
import { handleDebugCommand } from "./play/debug/commands";
|
|
4
8
|
import { createServerManager } from "./utils/server-manager";
|
|
5
9
|
import { parseMcCommand } from "./utils/command-router";
|
|
6
10
|
import { handleStatus } from "./handlers/status";
|
|
@@ -21,13 +25,31 @@ export default definePlugin({
|
|
|
21
25
|
description: "Minecraft服务器与QQ群消息互通插件",
|
|
22
26
|
|
|
23
27
|
async setup(ctx: MiokiContext) {
|
|
24
|
-
const configService = ctx.
|
|
28
|
+
const configService = getService(ctx, Services.Config);
|
|
25
29
|
|
|
26
30
|
const configHandler = createConfigHandler(configService);
|
|
27
31
|
await configHandler.register();
|
|
28
32
|
|
|
29
33
|
const config = configHandler.getConfig();
|
|
30
34
|
|
|
35
|
+
const playConfigHandler = createPlayConfigHandler(configService);
|
|
36
|
+
await playConfigHandler.register();
|
|
37
|
+
|
|
38
|
+
const aiService = getService(ctx, Services.AI);
|
|
39
|
+
|
|
40
|
+
const playManager = createPlayManager({
|
|
41
|
+
ctx,
|
|
42
|
+
aiService,
|
|
43
|
+
configService,
|
|
44
|
+
playConfigHandler,
|
|
45
|
+
syncConfigHandler: configHandler,
|
|
46
|
+
});
|
|
47
|
+
ctx.logger.info("Minecraft 游玩子系统已就绪");
|
|
48
|
+
|
|
49
|
+
if (aiService) {
|
|
50
|
+
aiService.registerSkill(createMcSkill(playManager));
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
const serverManager = createServerManager(
|
|
32
54
|
(serverName, status) => {
|
|
33
55
|
ctx.logger.info(`[MC] 服务器 ${serverName} 状态: ${status}`);
|
|
@@ -42,12 +64,32 @@ export default definePlugin({
|
|
|
42
64
|
|
|
43
65
|
serverManager.startServers(config);
|
|
44
66
|
|
|
45
|
-
ctx.handle("message", async (event
|
|
67
|
+
ctx.handle("message", async (event) => {
|
|
46
68
|
const text = ctx.text(event).trim();
|
|
69
|
+
const groupId =
|
|
70
|
+
"group_id" in event && typeof event.group_id === "number"
|
|
71
|
+
? event.group_id
|
|
72
|
+
: undefined;
|
|
73
|
+
|
|
74
|
+
if (groupId) {
|
|
75
|
+
const reply = await handleDebugCommand({
|
|
76
|
+
text,
|
|
77
|
+
isOwner: ctx.isOwner?.(event) ?? false,
|
|
78
|
+
debugEnabled: playConfigHandler.getConfig().debug.enabled,
|
|
79
|
+
playManager,
|
|
80
|
+
groupId: Number(groupId),
|
|
81
|
+
});
|
|
82
|
+
if (reply !== null) {
|
|
83
|
+
await event.reply(reply);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
47
88
|
const parsed = parseMcCommand(text);
|
|
48
89
|
|
|
49
90
|
if (!parsed || parsed.action === "") {
|
|
50
|
-
if (
|
|
91
|
+
if (groupId) {
|
|
92
|
+
playManager.onQqMessage(event);
|
|
51
93
|
await forwardToMc(ctx, event, config, configHandler, serverManager);
|
|
52
94
|
}
|
|
53
95
|
return;
|
|
@@ -55,7 +97,9 @@ export default definePlugin({
|
|
|
55
97
|
|
|
56
98
|
switch (parsed.action) {
|
|
57
99
|
case "状态": {
|
|
58
|
-
await handleStatus(serverManager, config, (msg) =>
|
|
100
|
+
await handleStatus(serverManager, config, async (msg) => {
|
|
101
|
+
await event.reply(msg);
|
|
102
|
+
});
|
|
59
103
|
break;
|
|
60
104
|
}
|
|
61
105
|
case "开启同步": {
|
|
@@ -66,7 +110,9 @@ export default definePlugin({
|
|
|
66
110
|
serverManager,
|
|
67
111
|
configHandler,
|
|
68
112
|
config,
|
|
69
|
-
(msg) =>
|
|
113
|
+
async (msg) => {
|
|
114
|
+
await event.reply(msg);
|
|
115
|
+
},
|
|
70
116
|
);
|
|
71
117
|
break;
|
|
72
118
|
}
|
|
@@ -78,13 +124,17 @@ export default definePlugin({
|
|
|
78
124
|
serverManager,
|
|
79
125
|
configHandler,
|
|
80
126
|
config,
|
|
81
|
-
(msg) =>
|
|
127
|
+
async (msg) => {
|
|
128
|
+
await event.reply(msg);
|
|
129
|
+
},
|
|
82
130
|
);
|
|
83
131
|
break;
|
|
84
132
|
}
|
|
85
133
|
case "重连": {
|
|
86
134
|
if (ctx.isOwner?.(event)) {
|
|
87
|
-
await handleReconnect(serverManager, (msg) =>
|
|
135
|
+
await handleReconnect(serverManager, async (msg) => {
|
|
136
|
+
await event.reply(msg);
|
|
137
|
+
});
|
|
88
138
|
}
|
|
89
139
|
break;
|
|
90
140
|
}
|
|
@@ -93,8 +143,10 @@ export default definePlugin({
|
|
|
93
143
|
|
|
94
144
|
ctx.logger.info("Minecraft插件加载成功");
|
|
95
145
|
|
|
96
|
-
return () => {
|
|
146
|
+
return async () => {
|
|
97
147
|
serverManager.stopServers();
|
|
148
|
+
await playManager.dispose();
|
|
149
|
+
if (aiService) aiService.removeSkill("mc");
|
|
98
150
|
ctx.logger.info("Minecraft插件已卸载");
|
|
99
151
|
};
|
|
100
152
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mioku-plugin-mc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Minecraft与QQ群消息互通插件,支持服务器状态监控与RCON命令",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"mioku": {
|
|
15
15
|
"services": [
|
|
16
|
-
"config"
|
|
16
|
+
"config",
|
|
17
|
+
"ai"
|
|
17
18
|
],
|
|
18
19
|
"accessHooks": [
|
|
19
20
|
{
|
|
@@ -63,7 +64,11 @@
|
|
|
63
64
|
}
|
|
64
65
|
},
|
|
65
66
|
"dependencies": {
|
|
66
|
-
"@cikeyqi/queqiao-node-sdk": "^0.1.3"
|
|
67
|
+
"@cikeyqi/queqiao-node-sdk": "^0.1.3",
|
|
68
|
+
"minecraft-data": "^3.111.0",
|
|
69
|
+
"mineflayer": "^4.37.1",
|
|
70
|
+
"vec3": "^0.2.0",
|
|
71
|
+
"zod": "^3.24.1"
|
|
67
72
|
},
|
|
68
73
|
"peerDependencies": {
|
|
69
74
|
"mioku": "^0.8.0",
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { MissionErrorCode } from "../state/mode";
|
|
3
|
+
import type { PlayServerConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface ActionOutcome {
|
|
6
|
+
action: string;
|
|
7
|
+
status: "succeeded" | "failed";
|
|
8
|
+
code?: MissionErrorCode;
|
|
9
|
+
detail: string;
|
|
10
|
+
data?: unknown;
|
|
11
|
+
at: number;
|
|
12
|
+
directiveId?: string;
|
|
13
|
+
completesDirectiveOnSuccess: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ActionContext {
|
|
17
|
+
bot: any;
|
|
18
|
+
server: PlayServerConfig;
|
|
19
|
+
stopCurrentTask: (reason?: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ActionDefinition {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
paramsSchema: z.ZodTypeAny;
|
|
26
|
+
run(
|
|
27
|
+
ctx: ActionContext,
|
|
28
|
+
params: any,
|
|
29
|
+
): Promise<{ detail: string; data?: unknown }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ActionRegistry {
|
|
33
|
+
private actions = new Map<string, ActionDefinition>();
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
this.register(defaultDropItemAction);
|
|
37
|
+
this.register(defaultSendCommandAction);
|
|
38
|
+
this.register(defaultStopTaskAction);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
register(action: ActionDefinition): void {
|
|
42
|
+
this.actions.set(action.name, action);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
list(): Array<{ name: string; description: string }> {
|
|
46
|
+
return [...this.actions.values()]
|
|
47
|
+
.map((action) => ({ name: action.name, description: action.description }))
|
|
48
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async execute(
|
|
52
|
+
name: string,
|
|
53
|
+
params: unknown,
|
|
54
|
+
ctx: ActionContext,
|
|
55
|
+
meta: { directiveId?: string; completesDirectiveOnSuccess?: boolean } = {},
|
|
56
|
+
): Promise<ActionOutcome> {
|
|
57
|
+
const action = this.actions.get(name);
|
|
58
|
+
if (!action) {
|
|
59
|
+
return failed(name, "unknown", `未知动作: ${name}`, meta);
|
|
60
|
+
}
|
|
61
|
+
const parsed = action.paramsSchema.safeParse(params ?? {});
|
|
62
|
+
if (!parsed.success) {
|
|
63
|
+
return failed(
|
|
64
|
+
name,
|
|
65
|
+
"unknown",
|
|
66
|
+
parsed.error.issues
|
|
67
|
+
.map(
|
|
68
|
+
(issue) => `${issue.path.join(".") || "params"}: ${issue.message}`,
|
|
69
|
+
)
|
|
70
|
+
.join("; "),
|
|
71
|
+
meta,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await action.run(ctx, parsed.data);
|
|
76
|
+
return {
|
|
77
|
+
action: name,
|
|
78
|
+
status: "succeeded",
|
|
79
|
+
detail: result.detail,
|
|
80
|
+
data: result.data,
|
|
81
|
+
at: Date.now(),
|
|
82
|
+
directiveId: meta.directiveId,
|
|
83
|
+
completesDirectiveOnSuccess: meta.completesDirectiveOnSuccess ?? true,
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const typed = normalizeActionError(error);
|
|
87
|
+
return failed(name, typed.code, typed.detail, meta);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const defaultDropItemAction: ActionDefinition = {
|
|
93
|
+
name: "drop_item",
|
|
94
|
+
description: "丢出指定数量的背包物品,可选先面向附近玩家。",
|
|
95
|
+
paramsSchema: z.object({
|
|
96
|
+
item: z.string().trim().min(1),
|
|
97
|
+
count: z.number().int().min(1).max(64).default(1),
|
|
98
|
+
target: z.string().trim().min(1).optional(),
|
|
99
|
+
}),
|
|
100
|
+
async run(ctx, params) {
|
|
101
|
+
const candidates =
|
|
102
|
+
ctx.bot.inventory
|
|
103
|
+
?.items?.()
|
|
104
|
+
.filter((item: any) => item.name === params.item) ?? [];
|
|
105
|
+
const available = candidates.reduce(
|
|
106
|
+
(sum: number, item: any) => sum + Number(item.count ?? 0),
|
|
107
|
+
0,
|
|
108
|
+
);
|
|
109
|
+
if (available < params.count) {
|
|
110
|
+
throw actionError(
|
|
111
|
+
"missing_item",
|
|
112
|
+
`物品 ${params.item} 不足,需要 ${params.count},现有 ${available}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (params.target) {
|
|
116
|
+
const player = ctx.bot.players?.[params.target]?.entity;
|
|
117
|
+
if (!player?.position)
|
|
118
|
+
throw actionError("target_not_found", `找不到玩家 ${params.target}`);
|
|
119
|
+
await ctx.bot.lookAt(player.position.offset(0, 1, 0), true);
|
|
120
|
+
}
|
|
121
|
+
let remaining = params.count;
|
|
122
|
+
for (const item of candidates) {
|
|
123
|
+
if (remaining <= 0) break;
|
|
124
|
+
const amount = Math.min(remaining, Number(item.count ?? 0));
|
|
125
|
+
await ctx.bot.toss(item.type, item.metadata ?? null, amount);
|
|
126
|
+
remaining -= amount;
|
|
127
|
+
}
|
|
128
|
+
return { detail: `已丢出 ${params.count} 个 ${params.item}`, data: params };
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const defaultSendCommandAction: ActionDefinition = {
|
|
133
|
+
name: "send_command",
|
|
134
|
+
description: "发送服务器允许列表中的斜杠命令。",
|
|
135
|
+
paramsSchema: z.object({ command: z.string().trim().min(2).max(256) }),
|
|
136
|
+
async run(ctx, params) {
|
|
137
|
+
const command = params.command.startsWith("/")
|
|
138
|
+
? params.command
|
|
139
|
+
: `/${params.command}`;
|
|
140
|
+
const allowed = ctx.server.allowedCommands.some(
|
|
141
|
+
(entry) => command === entry || command.startsWith(`${entry} `),
|
|
142
|
+
);
|
|
143
|
+
if (!allowed)
|
|
144
|
+
throw actionError("permission_denied", `命令不在允许列表中: ${command}`);
|
|
145
|
+
ctx.bot.chat(command);
|
|
146
|
+
return { detail: `已发送命令 ${command}` };
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const defaultStopTaskAction: ActionDefinition = {
|
|
151
|
+
name: "stop_current_task",
|
|
152
|
+
description: "停止当前高层任务并回到 idle。",
|
|
153
|
+
paramsSchema: z.object({ reason: z.string().trim().max(256).optional() }),
|
|
154
|
+
async run(ctx, params) {
|
|
155
|
+
ctx.stopCurrentTask(params.reason ?? "work_action_stop");
|
|
156
|
+
return { detail: "已停止当前任务" };
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
function actionError(
|
|
161
|
+
code: MissionErrorCode,
|
|
162
|
+
detail: string,
|
|
163
|
+
): Error & { code: MissionErrorCode } {
|
|
164
|
+
return Object.assign(new Error(detail), { code });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeActionError(error: unknown): {
|
|
168
|
+
code: MissionErrorCode;
|
|
169
|
+
detail: string;
|
|
170
|
+
} {
|
|
171
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
172
|
+
const typed = error as { code: MissionErrorCode; message?: string };
|
|
173
|
+
return {
|
|
174
|
+
code: typed.code,
|
|
175
|
+
detail: String(typed.message || error),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return { code: "unknown", detail: String(error) };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function failed(
|
|
182
|
+
action: string,
|
|
183
|
+
code: MissionErrorCode,
|
|
184
|
+
detail: string,
|
|
185
|
+
meta: { directiveId?: string; completesDirectiveOnSuccess?: boolean },
|
|
186
|
+
): ActionOutcome {
|
|
187
|
+
return {
|
|
188
|
+
action,
|
|
189
|
+
status: "failed",
|
|
190
|
+
code,
|
|
191
|
+
detail,
|
|
192
|
+
at: Date.now(),
|
|
193
|
+
directiveId: meta.directiveId,
|
|
194
|
+
completesDirectiveOnSuccess: meta.completesDirectiveOnSuccess ?? true,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function stableStringify(value: unknown): string {
|
|
2
|
+
return JSON.stringify(sortValue(value));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function sortValue(value: unknown): unknown {
|
|
6
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
7
|
+
if (!value || typeof value !== "object") return value;
|
|
8
|
+
const source = value as Record<string, unknown>;
|
|
9
|
+
const sorted: Record<string, unknown> = {};
|
|
10
|
+
for (const key of Object.keys(source).sort()) {
|
|
11
|
+
const next = source[key];
|
|
12
|
+
if (next !== undefined) sorted[key] = sortValue(next);
|
|
13
|
+
}
|
|
14
|
+
return sorted;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class SectionRevisionTracker {
|
|
18
|
+
private values = new Map<string, string>();
|
|
19
|
+
private revisions = new Map<string, number>();
|
|
20
|
+
|
|
21
|
+
revision(key: string, value: unknown): number {
|
|
22
|
+
const serialized = stableStringify(value);
|
|
23
|
+
if (this.values.get(key) !== serialized) {
|
|
24
|
+
this.values.set(key, serialized);
|
|
25
|
+
this.revisions.set(key, (this.revisions.get(key) ?? 0) + 1);
|
|
26
|
+
}
|
|
27
|
+
return this.revisions.get(key) ?? 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { PlayConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
type LoggerLike = {
|
|
4
|
+
info: (msg: string) => void;
|
|
5
|
+
warn: (msg: string) => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface AiLogRequest {
|
|
9
|
+
messages: Array<{ role: string; content: unknown }>;
|
|
10
|
+
tools?: Array<{ function?: { name?: string } } | undefined>;
|
|
11
|
+
temperature?: number;
|
|
12
|
+
max_tokens?: number;
|
|
13
|
+
trigger?: string;
|
|
14
|
+
triggerEvents?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AiLogResponse {
|
|
18
|
+
content?: string | null;
|
|
19
|
+
reasoning?: string | null;
|
|
20
|
+
toolCalls?: Array<{ name: string; arguments: string }>;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
error?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function logAiRequest(
|
|
26
|
+
logger: LoggerLike,
|
|
27
|
+
config: PlayConfig,
|
|
28
|
+
tag: "main" | "work" | "goodbye",
|
|
29
|
+
payload: AiLogRequest,
|
|
30
|
+
): void {
|
|
31
|
+
if (!config.debug.enabled) return;
|
|
32
|
+
const toolNames = (payload.tools ?? [])
|
|
33
|
+
.map((t) => t?.function?.name)
|
|
34
|
+
.filter((name): name is string => typeof name === "string")
|
|
35
|
+
.join(", ");
|
|
36
|
+
const lines: string[] = [];
|
|
37
|
+
lines.push(
|
|
38
|
+
`[MC/play] ▶ AI req (${tag})` +
|
|
39
|
+
` T=${payload.temperature ?? "?"}` +
|
|
40
|
+
` max=${payload.max_tokens ?? "?"}` +
|
|
41
|
+
(toolNames ? ` tools=[${toolNames}]` : "") +
|
|
42
|
+
(payload.trigger ? ` trigger=${payload.trigger}` : "") +
|
|
43
|
+
(Array.isArray(payload.triggerEvents)
|
|
44
|
+
? ` events=${payload.triggerEvents.length}`
|
|
45
|
+
: ""),
|
|
46
|
+
);
|
|
47
|
+
for (const message of payload.messages) {
|
|
48
|
+
const role = String(message.role ?? "unknown");
|
|
49
|
+
const text = stringifyContent(message.content);
|
|
50
|
+
lines.push(` ↳ ${role} (${text.length}c): ${truncate(text, 600)}`);
|
|
51
|
+
}
|
|
52
|
+
logger.info(lines.join("\n"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function logAiResponse(
|
|
56
|
+
logger: LoggerLike,
|
|
57
|
+
config: PlayConfig,
|
|
58
|
+
tag: "main" | "work" | "goodbye",
|
|
59
|
+
payload: AiLogResponse,
|
|
60
|
+
): void {
|
|
61
|
+
if (!config.debug.enabled) return;
|
|
62
|
+
if (payload.error !== undefined) {
|
|
63
|
+
logger.warn(
|
|
64
|
+
`[MC/play] ◀ AI resp (${tag}) failed in ${payload.durationMs}ms: ${stringifyError(payload.error)}`,
|
|
65
|
+
);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const parts: string[] = [];
|
|
69
|
+
parts.push(`[MC/play] ◀ AI resp (${tag}) ${payload.durationMs}ms`);
|
|
70
|
+
const contentText = safeText(payload.content);
|
|
71
|
+
if (contentText && contentText.length > 0) {
|
|
72
|
+
parts.push(`text=${truncate(contentText, 600)}`);
|
|
73
|
+
}
|
|
74
|
+
const reasoningText = safeText(payload.reasoning);
|
|
75
|
+
if (reasoningText && reasoningText.length > 0) {
|
|
76
|
+
parts.push(`reasoning=${truncate(reasoningText, 300)}`);
|
|
77
|
+
}
|
|
78
|
+
if (payload.toolCalls && payload.toolCalls.length > 0) {
|
|
79
|
+
for (const call of payload.toolCalls) {
|
|
80
|
+
const name = safeText(call.name) ?? "(unnamed)";
|
|
81
|
+
const args = safeText(call.arguments) ?? "{}";
|
|
82
|
+
parts.push(`tool=${name}(${truncate(args, 400)})`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (parts.length === 1) parts.push("(empty)");
|
|
86
|
+
logger.info(parts.join(" | "));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function stringifyContent(content: unknown): string {
|
|
90
|
+
if (typeof content === "string") return content;
|
|
91
|
+
if (content == null) return "";
|
|
92
|
+
if (Array.isArray(content)) {
|
|
93
|
+
return content
|
|
94
|
+
.map((part) => {
|
|
95
|
+
if (!part || typeof part !== "object") return String(part);
|
|
96
|
+
const obj = part as Record<string, unknown>;
|
|
97
|
+
if (obj.type === "text" && typeof obj.text === "string") return obj.text;
|
|
98
|
+
try {
|
|
99
|
+
return JSON.stringify(part);
|
|
100
|
+
} catch {
|
|
101
|
+
return String(part);
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
.join(" ");
|
|
105
|
+
}
|
|
106
|
+
if (typeof content === "object") {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.stringify(content);
|
|
109
|
+
} catch {
|
|
110
|
+
return "[unserializable]";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return String(content);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function safeText(value: unknown): string | null {
|
|
117
|
+
if (typeof value !== "string") return null;
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function truncate(text: unknown, max: number): string {
|
|
122
|
+
const raw = typeof text === "string" ? text : safeText(text) ?? "";
|
|
123
|
+
const collapsed = raw.replace(/\s+/g, " ").trim();
|
|
124
|
+
return collapsed.length > max ? collapsed.slice(0, max) + "…" : collapsed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function stringifyError(error: unknown): string {
|
|
128
|
+
if (error instanceof Error) return `${error.name}: ${error.message}`;
|
|
129
|
+
if (typeof error === "string") return error;
|
|
130
|
+
try {
|
|
131
|
+
return JSON.stringify(error);
|
|
132
|
+
} catch {
|
|
133
|
+
return String(error);
|
|
134
|
+
}
|
|
135
|
+
}
|