mioku-plugin-mc 2.1.0 → 3.0.0
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 +38 -2
- 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/runtime.ts +14 -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.ts +63 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { PlaySession } from "../session";
|
|
2
|
+
import type { PlayPluginContext } from "../context";
|
|
3
|
+
import type { GameChatLine } from "../bot/play-bus";
|
|
4
|
+
import {
|
|
5
|
+
buildMainSystemPrompt,
|
|
6
|
+
buildMainUserContext,
|
|
7
|
+
buildSessionFacts,
|
|
8
|
+
} from "./prompt";
|
|
9
|
+
import {
|
|
10
|
+
MAIN_TOOLS,
|
|
11
|
+
bindMainToolContext,
|
|
12
|
+
clearMainToolContext,
|
|
13
|
+
} from "./main-tools";
|
|
14
|
+
import { logAiRequest, logAiResponse } from "./debug-log";
|
|
15
|
+
import { withTimeoutMs } from "../util/async";
|
|
16
|
+
|
|
17
|
+
export interface MainLoopOptions {
|
|
18
|
+
session: PlaySession;
|
|
19
|
+
pluginCtx: PlayPluginContext;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MODEL_TIMEOUT_MS = 30_000;
|
|
23
|
+
|
|
24
|
+
interface QqChatLine {
|
|
25
|
+
text: string;
|
|
26
|
+
sender?: string;
|
|
27
|
+
atBot?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type QueueReason = "game_chat" | "qq_chat" | "chat_scan" | "work_completed";
|
|
31
|
+
|
|
32
|
+
interface QueuedMessage {
|
|
33
|
+
trigger: string;
|
|
34
|
+
reason: QueueReason;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class MainLoop {
|
|
38
|
+
private readonly session: PlaySession;
|
|
39
|
+
private readonly pluginCtx: PlayPluginContext;
|
|
40
|
+
private readonly systemPrompt: string;
|
|
41
|
+
private readonly sessionFacts: string;
|
|
42
|
+
private readonly toolSchemas: any[];
|
|
43
|
+
private cursor = 0;
|
|
44
|
+
private running = false;
|
|
45
|
+
private stopped = false;
|
|
46
|
+
private unsubscribeEvents?: () => void;
|
|
47
|
+
private focusedPlayer: string | null = null;
|
|
48
|
+
private focusedUntil = 0;
|
|
49
|
+
private pendingQueue: QueuedMessage[] = [];
|
|
50
|
+
private lastChatAt = 0;
|
|
51
|
+
private lastChatScanAt = 0;
|
|
52
|
+
|
|
53
|
+
constructor(opts: MainLoopOptions) {
|
|
54
|
+
this.session = opts.session;
|
|
55
|
+
this.pluginCtx = opts.pluginCtx;
|
|
56
|
+
const persona = opts.pluginCtx.mainInstance?.getPrompt("persona") ?? "";
|
|
57
|
+
this.systemPrompt = buildMainSystemPrompt({
|
|
58
|
+
persona,
|
|
59
|
+
bundles: opts.session.describeBundles(),
|
|
60
|
+
actions: opts.session.listActions(),
|
|
61
|
+
workStatus: opts.session.getWorkStatus(),
|
|
62
|
+
focusedUntil: 0,
|
|
63
|
+
});
|
|
64
|
+
this.sessionFacts = buildSessionFacts({
|
|
65
|
+
serverName: opts.session.server.name,
|
|
66
|
+
username: opts.session.server.username,
|
|
67
|
+
groupId: opts.session.binding.groupId,
|
|
68
|
+
maxPlayMs: opts.session.server.maxPlayMs,
|
|
69
|
+
allowedCommands: opts.session.server.allowedCommands,
|
|
70
|
+
});
|
|
71
|
+
this.toolSchemas = MAIN_TOOLS.map((tool) => ({
|
|
72
|
+
type: "function" as const,
|
|
73
|
+
function: {
|
|
74
|
+
name: tool.name,
|
|
75
|
+
description: tool.description,
|
|
76
|
+
parameters: tool.parameters,
|
|
77
|
+
},
|
|
78
|
+
})) as any[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
start(): void {
|
|
82
|
+
this.cursor = this.session.events.latestCursor();
|
|
83
|
+
this.session.bus.on("chat", (line: GameChatLine) => this.onGameChat(line));
|
|
84
|
+
this.unsubscribeEvents = this.session.events.subscribe((event) => {
|
|
85
|
+
if (event.type === "qq_chat") {
|
|
86
|
+
const data = event.data as QqChatLine | undefined;
|
|
87
|
+
if (this.shouldRespondToQq(data)) {
|
|
88
|
+
this.enqueue({
|
|
89
|
+
trigger: "direct_qq_chat",
|
|
90
|
+
reason: "qq_chat",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (event.type === "chat_scan_due") {
|
|
96
|
+
this.enqueue({ trigger: "chat_scan_due", reason: "chat_scan" });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (event.type === "work_completed") {
|
|
100
|
+
this.enqueue({ trigger: "work_completed", reason: "work_completed" });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
stop(): void {
|
|
107
|
+
this.stopped = true;
|
|
108
|
+
this.unsubscribeEvents?.();
|
|
109
|
+
clearMainToolContext();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private onGameChat(line: GameChatLine): void {
|
|
113
|
+
if (!this.shouldRespondToGameChat(line)) return;
|
|
114
|
+
if (line.username) {
|
|
115
|
+
this.focusedPlayer = line.username;
|
|
116
|
+
this.focusedUntil =
|
|
117
|
+
Date.now() + this.pluginCtx.getPlayConfig().mainConversationFocusMs;
|
|
118
|
+
}
|
|
119
|
+
this.enqueue({ trigger: "direct_game_chat", reason: "game_chat" });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private shouldRespondToGameChat(line: GameChatLine): boolean {
|
|
123
|
+
if (line.kind === "whisper") return true;
|
|
124
|
+
if (line.kind !== "chat" || !line.username) return false;
|
|
125
|
+
if (this.mentionsBot(line.text)) return true;
|
|
126
|
+
return (
|
|
127
|
+
this.focusedPlayer === line.username && Date.now() <= this.focusedUntil
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private shouldRespondToQq(data: QqChatLine | undefined): boolean {
|
|
132
|
+
if (!data?.text) return false;
|
|
133
|
+
if (data.atBot) return true;
|
|
134
|
+
if (this.mentionsBot(data.text)) return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private mentionsBot(text: string): boolean {
|
|
139
|
+
const botName = this.session.server.username.toLowerCase();
|
|
140
|
+
const lower = text.toLowerCase();
|
|
141
|
+
return lower.includes(botName) || lower.includes(`@${botName}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private enqueue(msg: QueuedMessage): void {
|
|
145
|
+
if (this.running) {
|
|
146
|
+
this.pendingQueue.push(msg);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
void this.runTurn(msg);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async runTurn(queued: QueuedMessage): Promise<void> {
|
|
153
|
+
if (this.stopped) return;
|
|
154
|
+
this.running = true;
|
|
155
|
+
try {
|
|
156
|
+
await this.runTurnInternal(queued);
|
|
157
|
+
|
|
158
|
+
while (this.pendingQueue.length > 0 && !this.stopped) {
|
|
159
|
+
const next = this.pendingQueue.shift()!;
|
|
160
|
+
await this.runTurnInternal(next);
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
this.running = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async runTurnInternal(queued: QueuedMessage): Promise<void> {
|
|
168
|
+
const main = this.pluginCtx.mainInstance;
|
|
169
|
+
const snapshot = this.session.getBehaviorSnapshot();
|
|
170
|
+
if (!main || !snapshot) return;
|
|
171
|
+
|
|
172
|
+
const batch = this.session.events.readAfter(this.cursor, isMainEvent, 80);
|
|
173
|
+
this.cursor = batch.cursor;
|
|
174
|
+
|
|
175
|
+
let context: string;
|
|
176
|
+
try {
|
|
177
|
+
context = buildMainUserContext({
|
|
178
|
+
trigger: queued.trigger,
|
|
179
|
+
events: batch.events,
|
|
180
|
+
snapshot,
|
|
181
|
+
workStatus: this.session.getWorkStatus(),
|
|
182
|
+
elapsedMs: Date.now() - this.session.startedAt,
|
|
183
|
+
maxMs: this.session.server.maxPlayMs,
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.pluginCtx.ctx.logger.error(
|
|
187
|
+
`[MC/play] main context 构建失败: ${error}`,
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const config = this.pluginCtx.getPlayConfig();
|
|
193
|
+
const startedAt = Date.now();
|
|
194
|
+
const messages = [
|
|
195
|
+
{ role: "system" as const, content: this.systemPrompt },
|
|
196
|
+
{ role: "user" as const, content: this.sessionFacts },
|
|
197
|
+
{ role: "user" as const, content: context },
|
|
198
|
+
];
|
|
199
|
+
bindMainToolContext({
|
|
200
|
+
session: this.session,
|
|
201
|
+
workSubroutineFactory: ({ session, goal, terminator, maxMs, maxIterations }) =>
|
|
202
|
+
this.pluginCtx.createWorkSubroutine({
|
|
203
|
+
session,
|
|
204
|
+
goal,
|
|
205
|
+
terminator,
|
|
206
|
+
maxMs,
|
|
207
|
+
maxIterations,
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
logAiRequest(this.pluginCtx.ctx.logger, config, "main", {
|
|
211
|
+
trigger: queued.trigger,
|
|
212
|
+
messages,
|
|
213
|
+
tools: MAIN_TOOLS as any[],
|
|
214
|
+
temperature: 0.8,
|
|
215
|
+
max_tokens: 700,
|
|
216
|
+
triggerEvents: batch.events,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
let response;
|
|
220
|
+
try {
|
|
221
|
+
response = await withTimeoutMs(
|
|
222
|
+
main.complete({
|
|
223
|
+
messages,
|
|
224
|
+
tools: this.toolSchemas,
|
|
225
|
+
temperature: 0.8,
|
|
226
|
+
max_tokens: 700,
|
|
227
|
+
}),
|
|
228
|
+
MODEL_TIMEOUT_MS,
|
|
229
|
+
);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logAiResponse(this.pluginCtx.ctx.logger, config, "main", {
|
|
232
|
+
durationMs: Date.now() - startedAt,
|
|
233
|
+
error,
|
|
234
|
+
});
|
|
235
|
+
this.pluginCtx.ctx.logger.warn(`[MC/play] 主模型调用失败: ${error}`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (!response) {
|
|
239
|
+
this.pluginCtx.ctx.logger.warn(
|
|
240
|
+
"[MC/play] 主模型返回为空,跳过本轮",
|
|
241
|
+
);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
logAiResponse(this.pluginCtx.ctx.logger, config, "main", {
|
|
245
|
+
durationMs: Date.now() - startedAt,
|
|
246
|
+
content: response.content ?? null,
|
|
247
|
+
reasoning: response.reasoning ?? null,
|
|
248
|
+
toolCalls: response.toolCalls ?? [],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const content = response.content?.trim() ?? "";
|
|
252
|
+
if (content.length > 0) {
|
|
253
|
+
await this.sendChatLines(splitChatLines(content));
|
|
254
|
+
}
|
|
255
|
+
if (queued.reason === "chat_scan") {
|
|
256
|
+
this.lastChatScanAt = Date.now();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async sendChatLines(lines: string[]): Promise<void> {
|
|
261
|
+
const config = this.pluginCtx.getPlayConfig();
|
|
262
|
+
for (const line of lines) {
|
|
263
|
+
const minInterval = config.gameChatMinIntervalMs;
|
|
264
|
+
const wait = minInterval - (Date.now() - this.lastChatAt);
|
|
265
|
+
if (wait > 0) {
|
|
266
|
+
await new Promise<void>((resolve) => setTimeout(resolve, wait));
|
|
267
|
+
}
|
|
268
|
+
this.session.say(line);
|
|
269
|
+
this.lastChatAt = Date.now();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
markChatScanDue(now = Date.now()): void {
|
|
274
|
+
if (now - this.lastChatScanAt < this.pluginCtx.getPlayConfig().chatScanIntervalMs) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.session.events.append("chat_scan_due", { at: now });
|
|
278
|
+
this.lastChatScanAt = now;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isMainEvent(event: { type: string }): boolean {
|
|
283
|
+
return [
|
|
284
|
+
"game_chat",
|
|
285
|
+
"qq_chat",
|
|
286
|
+
"damage",
|
|
287
|
+
"vitals_threshold",
|
|
288
|
+
"day_phase",
|
|
289
|
+
"death",
|
|
290
|
+
"respawn",
|
|
291
|
+
"inventory_change",
|
|
292
|
+
"equipment_change",
|
|
293
|
+
"mission_outcome",
|
|
294
|
+
"action_outcome",
|
|
295
|
+
"path_error",
|
|
296
|
+
].includes(event.type);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function splitChatLines(content: string): string[] {
|
|
300
|
+
return content
|
|
301
|
+
.split(/\r?\n+/)
|
|
302
|
+
.map((line) => line.trim())
|
|
303
|
+
.filter((line) => line.length > 0)
|
|
304
|
+
.slice(0, 3)
|
|
305
|
+
.map((line) => line.slice(0, 256));
|
|
306
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { AITool, SessionToolDefinition } from "mioku";
|
|
2
|
+
import type { PlaySession } from "../session";
|
|
3
|
+
import type { WorkSubroutine } from "./work-subroutine";
|
|
4
|
+
|
|
5
|
+
export interface MainToolContext {
|
|
6
|
+
session: PlaySession;
|
|
7
|
+
workSubroutineFactory: (opts: {
|
|
8
|
+
session: PlaySession;
|
|
9
|
+
goal: string;
|
|
10
|
+
terminator: WorkSubroutine["options"]["terminator"];
|
|
11
|
+
maxMs?: number;
|
|
12
|
+
maxIterations?: number;
|
|
13
|
+
}) => WorkSubroutine;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const START_MOTION_TOOL: AITool = {
|
|
17
|
+
name: "start_motion",
|
|
18
|
+
description:
|
|
19
|
+
"Start a behavior bundle (a high-level task). The bot's behavior engine will run it autonomously. " +
|
|
20
|
+
"Returns success/missionId if accepted, or rejection reason. Use this when you want the bot to do something multi-step.",
|
|
21
|
+
parameters: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
bundle: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description:
|
|
27
|
+
"Bundle id from the available bundles list, e.g. 'task.gather_resource' or 'task.follow_player'.",
|
|
28
|
+
},
|
|
29
|
+
params: {
|
|
30
|
+
type: "object",
|
|
31
|
+
description: "Bundle-specific parameters object.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ["bundle"],
|
|
35
|
+
},
|
|
36
|
+
handler: async (args, _event) => {
|
|
37
|
+
const ctx: MainToolContext | undefined = (handlerArgs as any).toolCtx;
|
|
38
|
+
if (!ctx) return { error: "tool context missing" };
|
|
39
|
+
const bundle = String(args?.bundle ?? "").trim();
|
|
40
|
+
if (!bundle) return { error: "缺少 bundle" };
|
|
41
|
+
const params = (args?.params ?? {}) as Record<string, unknown>;
|
|
42
|
+
const result = ctx.session.startMission({ bundle, params });
|
|
43
|
+
if (result.kind === "applied") {
|
|
44
|
+
return {
|
|
45
|
+
success: true,
|
|
46
|
+
missionId: result.missionId,
|
|
47
|
+
bundleId: result.bundleId,
|
|
48
|
+
message: result.message,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
rejected: true,
|
|
54
|
+
reason: result.reason,
|
|
55
|
+
detail: result.detail,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const STOP_MOTION_TOOL: AITool = {
|
|
61
|
+
name: "stop_motion",
|
|
62
|
+
description:
|
|
63
|
+
"Stop the currently running mission/bundle. No-op if nothing is running. Use when the current task should be cancelled.",
|
|
64
|
+
parameters: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
reason: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Optional reason for stopping (recorded in logs).",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
handler: async (args, _event) => {
|
|
74
|
+
const ctx: MainToolContext | undefined = (handlerArgs as any).toolCtx;
|
|
75
|
+
if (!ctx) return { error: "tool context missing" };
|
|
76
|
+
const result = ctx.session.stopMission(
|
|
77
|
+
typeof args?.reason === "string" ? args.reason : "main_stop",
|
|
78
|
+
);
|
|
79
|
+
if (result.kind === "applied") {
|
|
80
|
+
return { success: true, message: result.message };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
reason: result.reason,
|
|
85
|
+
detail: result.detail,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const PERFORM_ACTION_TOOL: AITool = {
|
|
91
|
+
name: "perform_action",
|
|
92
|
+
description:
|
|
93
|
+
"Execute a one-shot atomic action (drop item, send allowed server command, stop current task). " +
|
|
94
|
+
"Use for immediate actions that don't need multi-step planning.",
|
|
95
|
+
parameters: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
action: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "Action name: 'drop_item' | 'send_command' | 'stop_current_task'.",
|
|
101
|
+
},
|
|
102
|
+
params: {
|
|
103
|
+
type: "object",
|
|
104
|
+
description: "Action-specific parameters.",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
required: ["action"],
|
|
108
|
+
},
|
|
109
|
+
handler: async (args, _event) => {
|
|
110
|
+
const ctx: MainToolContext | undefined = (handlerArgs as any).toolCtx;
|
|
111
|
+
if (!ctx) return { error: "tool context missing" };
|
|
112
|
+
const action = String(args?.action ?? "").trim();
|
|
113
|
+
if (!action) return { error: "缺少 action" };
|
|
114
|
+
const params = (args?.params ?? {}) as Record<string, unknown>;
|
|
115
|
+
const outcome = await ctx.session.performAction(action, params);
|
|
116
|
+
return {
|
|
117
|
+
status: outcome.status,
|
|
118
|
+
code: outcome.code,
|
|
119
|
+
detail: outcome.detail,
|
|
120
|
+
data: outcome.data,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const DELEGATE_WORK_TOOL: AITool = {
|
|
126
|
+
name: "delegate_work",
|
|
127
|
+
description:
|
|
128
|
+
"Delegate a focused sub-task to the work agent. The work agent runs a synchronous sub-loop " +
|
|
129
|
+
"internally (multiple iterations, possibly up to 5 minutes) until the goal is complete, times out, " +
|
|
130
|
+
"or is cancelled. You will receive a final status report. Use this for tasks that need autonomous " +
|
|
131
|
+
"execution over time (e.g. 'gather 16 oak logs', 'follow player X for 2 minutes'). " +
|
|
132
|
+
"Do NOT use this for short actions — call start_motion or perform_action directly instead.",
|
|
133
|
+
parameters: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
goal: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description:
|
|
139
|
+
"Short high-level goal, e.g. 'gather 16 oak logs', 'follow kunkun for 2 minutes'.",
|
|
140
|
+
},
|
|
141
|
+
terminator: {
|
|
142
|
+
type: "object",
|
|
143
|
+
description:
|
|
144
|
+
"When to consider the goal complete. One of: " +
|
|
145
|
+
"{type: 'inventory_at_least', item, count}, " +
|
|
146
|
+
"{type: 'follow_for', target, ms}, " +
|
|
147
|
+
"{type: 'duration', ms}, " +
|
|
148
|
+
"{type: 'manual'} (you'll stop it next time).",
|
|
149
|
+
properties: {
|
|
150
|
+
type: {
|
|
151
|
+
type: "string",
|
|
152
|
+
enum: [
|
|
153
|
+
"inventory_at_least",
|
|
154
|
+
"follow_for",
|
|
155
|
+
"duration",
|
|
156
|
+
"manual",
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
item: { type: "string" },
|
|
160
|
+
count: { type: "number" },
|
|
161
|
+
target: { type: "string" },
|
|
162
|
+
ms: { type: "number" },
|
|
163
|
+
},
|
|
164
|
+
required: ["type"],
|
|
165
|
+
},
|
|
166
|
+
maxMs: {
|
|
167
|
+
type: "number",
|
|
168
|
+
description: "Hard timeout in ms. Default: 300000 (5 minutes).",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
required: ["goal", "terminator"],
|
|
172
|
+
},
|
|
173
|
+
handler: async (args, _event) => {
|
|
174
|
+
const ctx: MainToolContext | undefined = (handlerArgs as any).toolCtx;
|
|
175
|
+
if (!ctx) return { error: "tool context missing" };
|
|
176
|
+
const goal = String(args?.goal ?? "").trim();
|
|
177
|
+
const terminator = args?.terminator;
|
|
178
|
+
const maxMs = typeof args?.maxMs === "number" ? args.maxMs : undefined;
|
|
179
|
+
if (!goal) return { error: "缺少 goal" };
|
|
180
|
+
if (!terminator || typeof terminator !== "object") {
|
|
181
|
+
return { error: "缺少 terminator" };
|
|
182
|
+
}
|
|
183
|
+
const sub = ctx.workSubroutineFactory({
|
|
184
|
+
session: ctx.session,
|
|
185
|
+
goal,
|
|
186
|
+
terminator: terminator as any,
|
|
187
|
+
maxMs,
|
|
188
|
+
});
|
|
189
|
+
return await sub.run();
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const LEAVE_SERVER_TOOL: AITool = {
|
|
194
|
+
name: "leave_server",
|
|
195
|
+
description:
|
|
196
|
+
"Disconnect the bot from the current Minecraft server and end the play session. " +
|
|
197
|
+
"A short goodbye will be sent. Use when the player asks to leave or you decide the session should end.",
|
|
198
|
+
parameters: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {},
|
|
201
|
+
},
|
|
202
|
+
handler: async (_args, _event) => {
|
|
203
|
+
const ctx: MainToolContext | undefined = (handlerArgs as any).toolCtx;
|
|
204
|
+
if (!ctx) return { error: "tool context missing" };
|
|
205
|
+
await ctx.session.stop("main_leave_requested");
|
|
206
|
+
return { success: true, message: "已离开服务器" };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const MAIN_TOOLS: AITool[] = [
|
|
211
|
+
START_MOTION_TOOL,
|
|
212
|
+
STOP_MOTION_TOOL,
|
|
213
|
+
PERFORM_ACTION_TOOL,
|
|
214
|
+
DELEGATE_WORK_TOOL,
|
|
215
|
+
LEAVE_SERVER_TOOL,
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
let handlerArgs: { toolCtx?: MainToolContext } = {};
|
|
219
|
+
|
|
220
|
+
export function bindMainToolContext(toolCtx: MainToolContext): void {
|
|
221
|
+
handlerArgs.toolCtx = toolCtx;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function clearMainToolContext(): void {
|
|
225
|
+
handlerArgs.toolCtx = undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function buildMainSessionTools(): SessionToolDefinition[] {
|
|
229
|
+
return MAIN_TOOLS.map((tool) => ({ name: tool.name, tool }));
|
|
230
|
+
}
|