mioku-plugin-mc 2.0.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 +26 -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,207 @@
|
|
|
1
|
+
import type { ActionOutcome } from "../actions/registry";
|
|
2
|
+
import type { PlayEvent } from "../state/event-journal";
|
|
3
|
+
import type { BehaviorSnapshot } from "../state/snapshot";
|
|
4
|
+
import type { PlayServerConfig, WorkStatus } from "../types";
|
|
5
|
+
import { stableStringify } from "./context-builder";
|
|
6
|
+
import type { WorkTerminator } from "./work-subroutine";
|
|
7
|
+
|
|
8
|
+
export function buildGoodbyePrompt(
|
|
9
|
+
persona: string,
|
|
10
|
+
server: PlayServerConfig,
|
|
11
|
+
): string {
|
|
12
|
+
const lines: string[] = [];
|
|
13
|
+
if (persona) lines.push("## Persona", persona, "");
|
|
14
|
+
lines.push(
|
|
15
|
+
"## Task",
|
|
16
|
+
`You are leaving the Minecraft server \"${server.name}\" right now.`,
|
|
17
|
+
"Say a short in-character goodbye to the players in the server chat.",
|
|
18
|
+
"1 or 2 short lines. No markers, no quotes, no preamble.",
|
|
19
|
+
"Output ONLY the goodbye text.",
|
|
20
|
+
);
|
|
21
|
+
return lines.join("\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MainPromptContext {
|
|
25
|
+
persona: string;
|
|
26
|
+
bundles: unknown;
|
|
27
|
+
actions: unknown;
|
|
28
|
+
workStatus: WorkStatus | null;
|
|
29
|
+
focusedUntil: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildMainSystemPrompt(input: MainPromptContext): string {
|
|
33
|
+
return [
|
|
34
|
+
"You are the only AI for a Minecraft bot that is currently in-game.",
|
|
35
|
+
"You see the bot's vitals, inventory, equipment, nearby entities, world state, recent chat, and (if running) the work subroutine's status.",
|
|
36
|
+
"",
|
|
37
|
+
"## Your output IS the chat reply",
|
|
38
|
+
"- Your plain-text response will be split by newlines and sent to the in-game chat, one line per newline.",
|
|
39
|
+
"- Keep total text short (max 3 short lines, each <=256 chars).",
|
|
40
|
+
"- Empty text is fine when silence is the right answer.",
|
|
41
|
+
"- DO NOT output JSON, tool-call syntax, or anything else. Just natural language chat lines.",
|
|
42
|
+
"",
|
|
43
|
+
"## How to act",
|
|
44
|
+
"- You have tools. Each tool call is one action. The bot executes tools synchronously — you wait for completion before your next move.",
|
|
45
|
+
"- For multi-step autonomous tasks (e.g. gather 16 oak logs, follow player X for 2 minutes), use `delegate_work` so the work agent runs the sub-task and returns a status.",
|
|
46
|
+
"- For short or immediate actions, call `start_motion`, `perform_action`, or `stop_motion` directly.",
|
|
47
|
+
"- Use `leave_server` only when the player asks to leave or the session must end.",
|
|
48
|
+
"",
|
|
49
|
+
"## Focus rules",
|
|
50
|
+
"- If a player asked you to do something, STAY ON THAT TASK. Do not switch to a different topic just because new chat arrived.",
|
|
51
|
+
"- You are NOT interrupted by in-game events (damage, hostile mobs, equipment loss). Your behavior engine handles survival automatically; you keep planning.",
|
|
52
|
+
"- If a new message explicitly @-mentions the bot while you are in the middle of a tool chain, your MainLoop will queue it; you will see and respond to it in your NEXT turn, after the current tool completes.",
|
|
53
|
+
"- Don't repeat yourself. Don't echo the player's message. Don't narrate actions in asterisks.",
|
|
54
|
+
"",
|
|
55
|
+
"## Chat scan context",
|
|
56
|
+
"- When you are triggered by `chat_scan_due` (every few minutes), review the recent chat lines. Reply ONLY if there's something that actually needs a response (someone asked you a question, or said something noteworthy). Otherwise output empty text and end your turn.",
|
|
57
|
+
"",
|
|
58
|
+
"## Reading the work status",
|
|
59
|
+
"- When a task is delegated to work, you can see its `summary` and `progress` passively. Use it to answer player questions like \"what are you doing?\" without re-running anything.",
|
|
60
|
+
"- Do NOT call `delegate_work` again while one is already running (`workStatus.running === true`). Either wait, or stop the current one first with `stop_motion`.",
|
|
61
|
+
"",
|
|
62
|
+
`Available motion bundles:\n${stableStringify(input.bundles)}`,
|
|
63
|
+
`Available atomic actions:\n${stableStringify(input.actions)}`,
|
|
64
|
+
"",
|
|
65
|
+
input.persona
|
|
66
|
+
? `Persona:\n${input.persona}`
|
|
67
|
+
: "Persona: act friendly, concise, and human-like.",
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface WorkPromptContext {
|
|
72
|
+
goal: string;
|
|
73
|
+
terminator: WorkTerminator;
|
|
74
|
+
bundles: unknown;
|
|
75
|
+
actions: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildWorkSystemPrompt(input: WorkPromptContext): string {
|
|
79
|
+
const terminatorDescription = describeTerminator(input.terminator);
|
|
80
|
+
return [
|
|
81
|
+
"You are the work agent for a Minecraft bot, called as a synchronous subroutine by the main agent.",
|
|
82
|
+
"Your single job is to drive the bot until the goal is complete, then return.",
|
|
83
|
+
"",
|
|
84
|
+
"## Your single assignment",
|
|
85
|
+
`Goal: ${input.goal}`,
|
|
86
|
+
`Terminator: ${terminatorDescription}`,
|
|
87
|
+
"",
|
|
88
|
+
"## How to act",
|
|
89
|
+
"- You have tools to start/stop motion bundles, perform atomic actions, and update status.",
|
|
90
|
+
"- Pick ONE next action per turn. Don't try to do multiple things at once.",
|
|
91
|
+
"- Do NOT output chat lines — only tool calls and (optionally) text that updates the status report.",
|
|
92
|
+
"- Call `update_status` only when progress changed meaningfully (e.g. collected 4 logs out of 16). Don't spam status updates.",
|
|
93
|
+
"- When the terminator condition is met (inventory threshold reached, follow duration elapsed, etc.), just stop calling new actions and end your turn — the subroutine will detect completion and return.",
|
|
94
|
+
"",
|
|
95
|
+
"## Failure handling",
|
|
96
|
+
"- If a motion/action outcome is failed or blocked, do NOT silently retry the same thing. Read the structured error code and adapt:",
|
|
97
|
+
" target_not_found → switch target or search elsewhere",
|
|
98
|
+
" resource_not_found → wander or give up",
|
|
99
|
+
" missing_tool → craft a prerequisite first",
|
|
100
|
+
" inventory_full → drop or deposit",
|
|
101
|
+
" path_unreachable → try another area",
|
|
102
|
+
" path_timeout → break line of sight and try again",
|
|
103
|
+
" permission_denied → stop the goal",
|
|
104
|
+
" disconnected → wait briefly",
|
|
105
|
+
"",
|
|
106
|
+
"Stable mission error codes include target_not_found, target_lost, resource_not_found, missing_item, missing_tool, inventory_full, path_unreachable, path_timeout, permission_denied, command_rejected, disconnected, cancelled, and unknown.",
|
|
107
|
+
"",
|
|
108
|
+
`Available motion bundles:\n${stableStringify(input.bundles)}`,
|
|
109
|
+
`Available atomic actions:\n${stableStringify(input.actions)}`,
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function describeTerminator(t: WorkTerminator): string {
|
|
114
|
+
switch (t.type) {
|
|
115
|
+
case "inventory_at_least":
|
|
116
|
+
return `inventory has at least ${t.count} × ${t.item}`;
|
|
117
|
+
case "follow_for":
|
|
118
|
+
return `follow ${t.target} for ${t.ms} ms`;
|
|
119
|
+
case "duration":
|
|
120
|
+
return `run for ${t.ms} ms total`;
|
|
121
|
+
case "manual":
|
|
122
|
+
return "main agent will stop you when done";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildSessionFacts(input: {
|
|
127
|
+
serverName: string;
|
|
128
|
+
username: string;
|
|
129
|
+
groupId: number;
|
|
130
|
+
maxPlayMs: number;
|
|
131
|
+
allowedCommands: string[];
|
|
132
|
+
}): string {
|
|
133
|
+
return `SESSION_FACTS\n${stableStringify(input)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface MainUserContextInput {
|
|
137
|
+
trigger: string;
|
|
138
|
+
events: PlayEvent[];
|
|
139
|
+
snapshot: BehaviorSnapshot;
|
|
140
|
+
workStatus: WorkStatus | null;
|
|
141
|
+
elapsedMs: number;
|
|
142
|
+
maxMs: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildMainUserContext(input: MainUserContextInput): string {
|
|
146
|
+
const snapshot = input.snapshot;
|
|
147
|
+
return `CURRENT_MAIN_CONTEXT\n${stableStringify({
|
|
148
|
+
trigger: input.trigger,
|
|
149
|
+
elapsedMs: input.elapsedMs,
|
|
150
|
+
maxMs: input.maxMs,
|
|
151
|
+
events: input.events,
|
|
152
|
+
workStatus: input.workStatus,
|
|
153
|
+
status: {
|
|
154
|
+
health: snapshot.vitals.health,
|
|
155
|
+
food: snapshot.vitals.food,
|
|
156
|
+
oxygen: snapshot.vitals.oxygen,
|
|
157
|
+
position: snapshot.position,
|
|
158
|
+
dimension: snapshot.dimension,
|
|
159
|
+
equipment: snapshot.equipment,
|
|
160
|
+
heldItem: snapshot.heldItem,
|
|
161
|
+
inventory: {
|
|
162
|
+
items: snapshot.inventory.items.slice(0, 24),
|
|
163
|
+
emptySlots: snapshot.inventory.emptySlots,
|
|
164
|
+
full: snapshot.inventory.full,
|
|
165
|
+
},
|
|
166
|
+
nearbyPlayers: snapshot.entities
|
|
167
|
+
.filter((entity) => entity.kind === "player")
|
|
168
|
+
.slice(0, 8),
|
|
169
|
+
nearbyHostiles: snapshot.entities
|
|
170
|
+
.filter((entity) => entity.kind === "hostile")
|
|
171
|
+
.slice(0, 8),
|
|
172
|
+
environment: {
|
|
173
|
+
timeOfDay: snapshot.environment.timeOfDay,
|
|
174
|
+
isDay: snapshot.environment.isDay,
|
|
175
|
+
weather: snapshot.environment.weather,
|
|
176
|
+
biome: snapshot.environment.biome,
|
|
177
|
+
terrain: {
|
|
178
|
+
below: snapshot.environment.terrain.below,
|
|
179
|
+
feet: snapshot.environment.terrain.feet,
|
|
180
|
+
head: snapshot.environment.terrain.head,
|
|
181
|
+
nearbyInteresting:
|
|
182
|
+
snapshot.environment.terrain.nearbyInteresting.slice(0, 12),
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
mission: snapshot.mission,
|
|
186
|
+
activeBehaviors: snapshot.activeBehaviors.filter((b) => b.active),
|
|
187
|
+
},
|
|
188
|
+
})}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface WorkUserContextInput {
|
|
192
|
+
goal: string;
|
|
193
|
+
terminator: WorkTerminator;
|
|
194
|
+
triggerEvents: PlayEvent[];
|
|
195
|
+
snapshot: BehaviorSnapshot;
|
|
196
|
+
lastActionOutcome: ActionOutcome | null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function buildWorkUserContext(input: WorkUserContextInput): string {
|
|
200
|
+
return `CURRENT_WORK_CONTEXT\n${stableStringify({
|
|
201
|
+
goal: input.goal,
|
|
202
|
+
terminator: input.terminator,
|
|
203
|
+
triggerEvents: input.triggerEvents,
|
|
204
|
+
lastActionOutcome: input.lastActionOutcome,
|
|
205
|
+
world: input.snapshot,
|
|
206
|
+
})}`;
|
|
207
|
+
}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import type { AIInstance, AITool, SessionToolDefinition } from "mioku";
|
|
2
|
+
import type { PlaySession } from "../session";
|
|
3
|
+
import type { PlayEvent } from "../state/event-journal";
|
|
4
|
+
import {
|
|
5
|
+
buildWorkSystemPrompt,
|
|
6
|
+
buildWorkUserContext,
|
|
7
|
+
} from "./prompt";
|
|
8
|
+
import { withTimeoutMs } from "../util/async";
|
|
9
|
+
import { logAiRequest, logAiResponse } from "./debug-log";
|
|
10
|
+
|
|
11
|
+
export type WorkTerminator =
|
|
12
|
+
| { type: "inventory_at_least"; item: string; count: number }
|
|
13
|
+
| { type: "follow_for"; target: string; ms: number }
|
|
14
|
+
| { type: "duration"; ms: number }
|
|
15
|
+
| { type: "manual" };
|
|
16
|
+
|
|
17
|
+
export interface WorkSubroutineOptions {
|
|
18
|
+
session: PlaySession;
|
|
19
|
+
goal: string;
|
|
20
|
+
terminator: WorkTerminator;
|
|
21
|
+
maxMs?: number;
|
|
22
|
+
maxIterations?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WorkSubroutineResult {
|
|
26
|
+
status: "completed" | "timeout" | "failed" | "cancelled";
|
|
27
|
+
summary: string;
|
|
28
|
+
iterations: number;
|
|
29
|
+
elapsedMs: number;
|
|
30
|
+
artifacts: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const WORK_MODEL_TIMEOUT_MS = 60_000;
|
|
34
|
+
|
|
35
|
+
export class WorkSubroutine {
|
|
36
|
+
private readonly session: PlaySession;
|
|
37
|
+
private readonly goal: string;
|
|
38
|
+
private readonly terminator: WorkTerminator;
|
|
39
|
+
private readonly maxMs: number;
|
|
40
|
+
private readonly maxIterations: number;
|
|
41
|
+
private readonly startedAt: number;
|
|
42
|
+
private status: "running" | "completed" | "timeout" | "failed" | "cancelled" =
|
|
43
|
+
"running";
|
|
44
|
+
private lastSummary = "";
|
|
45
|
+
private lastProgress: { current: number; target: number; unit: string } | undefined;
|
|
46
|
+
private toolCtxBound = false;
|
|
47
|
+
|
|
48
|
+
constructor(opts: WorkSubroutineOptions) {
|
|
49
|
+
this.session = opts.session;
|
|
50
|
+
this.goal = opts.goal;
|
|
51
|
+
this.terminator = opts.terminator;
|
|
52
|
+
this.maxMs = opts.maxMs ?? 5 * 60_000;
|
|
53
|
+
this.maxIterations = opts.maxIterations ?? 25;
|
|
54
|
+
this.startedAt = Date.now();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get options(): { goal: string; terminator: WorkTerminator } {
|
|
58
|
+
return { goal: this.goal, terminator: this.terminator };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async run(): Promise<WorkSubroutineResult> {
|
|
62
|
+
const work = this.session.getPluginCtx().workInstance;
|
|
63
|
+
if (!work) {
|
|
64
|
+
return {
|
|
65
|
+
status: "failed",
|
|
66
|
+
summary: "work AI 实例不可用",
|
|
67
|
+
iterations: 0,
|
|
68
|
+
elapsedMs: Date.now() - this.startedAt,
|
|
69
|
+
artifacts: {},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
bindWorkToolContext({
|
|
74
|
+
session: this.session,
|
|
75
|
+
onStatusUpdate: (summary, progress) => {
|
|
76
|
+
this.lastSummary = summary;
|
|
77
|
+
this.lastProgress = progress;
|
|
78
|
+
this.session.updateWorkStatus({
|
|
79
|
+
running: true,
|
|
80
|
+
goal: this.goal,
|
|
81
|
+
summary,
|
|
82
|
+
progress,
|
|
83
|
+
updatedAt: Date.now(),
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
this.toolCtxBound = true;
|
|
88
|
+
this.session.updateWorkStatus({
|
|
89
|
+
running: true,
|
|
90
|
+
goal: this.goal,
|
|
91
|
+
summary: `开始执行: ${this.goal}`,
|
|
92
|
+
updatedAt: Date.now(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const cursor = this.session.events.latestCursor();
|
|
96
|
+
let iterations = 0;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
while (iterations < this.maxIterations) {
|
|
100
|
+
if (Date.now() - this.startedAt > this.maxMs) {
|
|
101
|
+
this.status = "timeout";
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
if (this.isTerminatorMet()) {
|
|
105
|
+
this.status = "completed";
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (this.session.isStopped) {
|
|
109
|
+
this.status = "cancelled";
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const decision = await this.runOneTurn(work, cursor);
|
|
114
|
+
iterations += 1;
|
|
115
|
+
if (!decision) continue;
|
|
116
|
+
if (decision.fatal) {
|
|
117
|
+
this.status = "failed";
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (this.status === "running") {
|
|
122
|
+
this.status =
|
|
123
|
+
Date.now() - this.startedAt > this.maxMs ? "timeout" : "completed";
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
if (this.toolCtxBound) {
|
|
127
|
+
clearWorkToolContext();
|
|
128
|
+
this.toolCtxBound = false;
|
|
129
|
+
}
|
|
130
|
+
this.session.updateWorkStatus({
|
|
131
|
+
running: false,
|
|
132
|
+
goal: this.goal,
|
|
133
|
+
summary: this.lastSummary || this.statusDefaultSummary(),
|
|
134
|
+
progress: this.lastProgress,
|
|
135
|
+
updatedAt: Date.now(),
|
|
136
|
+
});
|
|
137
|
+
this.session.events.append("work_completed", {
|
|
138
|
+
goal: this.goal,
|
|
139
|
+
status: this.status,
|
|
140
|
+
iterations,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
status: this.status,
|
|
146
|
+
summary: this.lastSummary || this.statusDefaultSummary(),
|
|
147
|
+
iterations,
|
|
148
|
+
elapsedMs: Date.now() - this.startedAt,
|
|
149
|
+
artifacts: {
|
|
150
|
+
progress: this.lastProgress,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private statusDefaultSummary(): string {
|
|
156
|
+
switch (this.status) {
|
|
157
|
+
case "completed":
|
|
158
|
+
return `目标已完成: ${this.goal}`;
|
|
159
|
+
case "timeout":
|
|
160
|
+
return `目标超时: ${this.goal}`;
|
|
161
|
+
case "failed":
|
|
162
|
+
return `目标失败: ${this.goal}`;
|
|
163
|
+
case "cancelled":
|
|
164
|
+
return `目标取消: ${this.goal}`;
|
|
165
|
+
default:
|
|
166
|
+
return `目标进行中: ${this.goal}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private isTerminatorMet(): boolean {
|
|
171
|
+
const bot = this.session.getBot();
|
|
172
|
+
if (!bot) return false;
|
|
173
|
+
switch (this.terminator.type) {
|
|
174
|
+
case "inventory_at_least": {
|
|
175
|
+
const total = countInventoryItem(bot, this.terminator.item);
|
|
176
|
+
return total >= this.terminator.count;
|
|
177
|
+
}
|
|
178
|
+
case "follow_for":
|
|
179
|
+
case "duration":
|
|
180
|
+
case "manual":
|
|
181
|
+
return false;
|
|
182
|
+
default:
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async runOneTurn(
|
|
188
|
+
work: AIInstance,
|
|
189
|
+
cursor: number,
|
|
190
|
+
): Promise<{ fatal: boolean } | null> {
|
|
191
|
+
const snapshot = this.session.getBehaviorSnapshot();
|
|
192
|
+
if (!snapshot) return null;
|
|
193
|
+
|
|
194
|
+
const batch = this.session.events.readAfter(cursor, isWorkEvent, 50);
|
|
195
|
+
const triggerEvents = batch.events.length > 0 ? batch.events : [];
|
|
196
|
+
|
|
197
|
+
const context = buildWorkUserContext({
|
|
198
|
+
goal: this.goal,
|
|
199
|
+
terminator: this.terminator,
|
|
200
|
+
triggerEvents,
|
|
201
|
+
snapshot,
|
|
202
|
+
lastActionOutcome: this.session.getLastActionOutcome(),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const config = this.session.getPluginCtx().getPlayConfig();
|
|
206
|
+
const startedAt = Date.now();
|
|
207
|
+
const messages = [
|
|
208
|
+
{ role: "system" as const, content: this.buildWorkSystemPrompt() },
|
|
209
|
+
{ role: "user" as const, content: context },
|
|
210
|
+
];
|
|
211
|
+
logAiRequest(this.session.getPluginCtx().ctx.logger, config, "work", {
|
|
212
|
+
trigger: `iteration:${this.iterationsLabel()}`,
|
|
213
|
+
messages,
|
|
214
|
+
tools: WORK_TOOLS as any[],
|
|
215
|
+
temperature: 0.2,
|
|
216
|
+
max_tokens: 600,
|
|
217
|
+
triggerEvents,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
let response;
|
|
221
|
+
try {
|
|
222
|
+
response = await withTimeoutMs(
|
|
223
|
+
work.complete({
|
|
224
|
+
messages,
|
|
225
|
+
tools: WORK_TOOL_SCHEMAS,
|
|
226
|
+
temperature: 0.2,
|
|
227
|
+
max_tokens: 600,
|
|
228
|
+
}),
|
|
229
|
+
WORK_MODEL_TIMEOUT_MS,
|
|
230
|
+
);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
logAiResponse(this.session.getPluginCtx().ctx.logger, config, "work", {
|
|
233
|
+
durationMs: Date.now() - startedAt,
|
|
234
|
+
error,
|
|
235
|
+
});
|
|
236
|
+
this.session
|
|
237
|
+
.getPluginCtx()
|
|
238
|
+
.ctx.logger.warn(`[MC/play] 工作模型调用失败: ${error}`);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
if (!response) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
logAiResponse(this.session.getPluginCtx().ctx.logger, config, "work", {
|
|
245
|
+
durationMs: Date.now() - startedAt,
|
|
246
|
+
content: response.content ?? null,
|
|
247
|
+
reasoning: response.reasoning ?? null,
|
|
248
|
+
toolCalls: response.toolCalls ?? [],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
let toolCalls = response.toolCalls ?? [];
|
|
252
|
+
let content = response.content;
|
|
253
|
+
let lastFatal = false;
|
|
254
|
+
while (toolCalls.length > 0 || (content && content.trim().length > 0)) {
|
|
255
|
+
if (content && content.trim().length > 0) {
|
|
256
|
+
const trimmed = content.trim();
|
|
257
|
+
if (trimmed.length > 0 && !this.lastSummary) {
|
|
258
|
+
this.lastSummary = trimmed;
|
|
259
|
+
}
|
|
260
|
+
content = null;
|
|
261
|
+
}
|
|
262
|
+
if (toolCalls.length === 0) break;
|
|
263
|
+
const call = toolCalls.shift()!;
|
|
264
|
+
const result = await runWorkToolCall(call);
|
|
265
|
+
if (result.fatal) lastFatal = true;
|
|
266
|
+
if (result.outputText) {
|
|
267
|
+
this.lastSummary = result.outputText;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { fatal: lastFatal };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private iterationsLabel(): string {
|
|
275
|
+
return String(Math.floor((Date.now() - this.startedAt) / 1000));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private buildWorkSystemPrompt(): string {
|
|
279
|
+
const bundles = this.session.describeBundles();
|
|
280
|
+
const actions = this.session.listActions();
|
|
281
|
+
return buildWorkSystemPrompt({
|
|
282
|
+
goal: this.goal,
|
|
283
|
+
terminator: this.terminator,
|
|
284
|
+
bundles,
|
|
285
|
+
actions,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface WorkToolContext {
|
|
291
|
+
session: PlaySession;
|
|
292
|
+
onStatusUpdate: (
|
|
293
|
+
summary: string,
|
|
294
|
+
progress?: { current: number; target: number; unit: string },
|
|
295
|
+
) => void;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let workHandlerCtx: { ctx?: WorkToolContext } = {};
|
|
299
|
+
|
|
300
|
+
export function bindWorkToolContext(ctx: WorkToolContext): void {
|
|
301
|
+
workHandlerCtx.ctx = ctx;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function clearWorkToolContext(): void {
|
|
305
|
+
workHandlerCtx.ctx = undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const WORK_START_MOTION: AITool = {
|
|
309
|
+
name: "start_motion",
|
|
310
|
+
description:
|
|
311
|
+
"Start a behavior bundle. Returns success or rejection reason. Use only ONE bundle at a time — " +
|
|
312
|
+
"starting a new bundle cancels the previous one.",
|
|
313
|
+
parameters: {
|
|
314
|
+
type: "object",
|
|
315
|
+
properties: {
|
|
316
|
+
bundle: { type: "string" },
|
|
317
|
+
params: { type: "object" },
|
|
318
|
+
},
|
|
319
|
+
required: ["bundle"],
|
|
320
|
+
},
|
|
321
|
+
handler: async (args) => {
|
|
322
|
+
const ctx = workHandlerCtx.ctx;
|
|
323
|
+
if (!ctx) return { fatal: true, detail: "work tool context missing" };
|
|
324
|
+
const bundle = String(args?.bundle ?? "").trim();
|
|
325
|
+
const params = (args?.params ?? {}) as Record<string, unknown>;
|
|
326
|
+
if (!bundle) return { fatal: false, detail: "缺少 bundle" };
|
|
327
|
+
const result = ctx.session.startMission({ bundle, params });
|
|
328
|
+
if (result.kind === "applied") {
|
|
329
|
+
return { success: true, missionId: result.missionId, bundleId: result.bundleId };
|
|
330
|
+
}
|
|
331
|
+
return { success: false, rejected: true, reason: result.reason, detail: result.detail };
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const WORK_STOP_MOTION: AITool = {
|
|
336
|
+
name: "stop_motion",
|
|
337
|
+
description: "Stop the current bundle mission. No-op if nothing is running.",
|
|
338
|
+
parameters: {
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: { reason: { type: "string" } },
|
|
341
|
+
},
|
|
342
|
+
handler: async (args) => {
|
|
343
|
+
const ctx = workHandlerCtx.ctx;
|
|
344
|
+
if (!ctx) return { fatal: true };
|
|
345
|
+
const result = ctx.session.stopMission(
|
|
346
|
+
typeof args?.reason === "string" ? args.reason : "work_stop",
|
|
347
|
+
);
|
|
348
|
+
if (result.kind === "applied") return { success: true, message: result.message };
|
|
349
|
+
return { success: false, reason: result.reason, detail: result.detail };
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const WORK_PERFORM_ACTION: AITool = {
|
|
354
|
+
name: "perform_action",
|
|
355
|
+
description:
|
|
356
|
+
"Execute an atomic one-shot action (drop_item, send_command, stop_current_task).",
|
|
357
|
+
parameters: {
|
|
358
|
+
type: "object",
|
|
359
|
+
properties: {
|
|
360
|
+
action: { type: "string" },
|
|
361
|
+
params: { type: "object" },
|
|
362
|
+
},
|
|
363
|
+
required: ["action"],
|
|
364
|
+
},
|
|
365
|
+
handler: async (args) => {
|
|
366
|
+
const ctx = workHandlerCtx.ctx;
|
|
367
|
+
if (!ctx) return { fatal: true };
|
|
368
|
+
const action = String(args?.action ?? "").trim();
|
|
369
|
+
const params = (args?.params ?? {}) as Record<string, unknown>;
|
|
370
|
+
if (!action) return { fatal: false, detail: "缺少 action" };
|
|
371
|
+
const outcome = await ctx.session.performAction(action, params);
|
|
372
|
+
return {
|
|
373
|
+
status: outcome.status,
|
|
374
|
+
code: outcome.code,
|
|
375
|
+
detail: outcome.detail,
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const WORK_UPDATE_STATUS: AITool = {
|
|
381
|
+
name: "update_status",
|
|
382
|
+
description:
|
|
383
|
+
"Update the passive status report that the main agent can read on demand. " +
|
|
384
|
+
"Use sparingly — only when the user-visible progress changed meaningfully.",
|
|
385
|
+
parameters: {
|
|
386
|
+
type: "object",
|
|
387
|
+
properties: {
|
|
388
|
+
summary: {
|
|
389
|
+
type: "string",
|
|
390
|
+
description: "Short status text, e.g. '正在砍第 4 棵树'.",
|
|
391
|
+
},
|
|
392
|
+
progress: {
|
|
393
|
+
type: "object",
|
|
394
|
+
description: "Optional numeric progress {current, target, unit}.",
|
|
395
|
+
properties: {
|
|
396
|
+
current: { type: "number" },
|
|
397
|
+
target: { type: "number" },
|
|
398
|
+
unit: { type: "string" },
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
required: ["summary"],
|
|
403
|
+
},
|
|
404
|
+
handler: async (args) => {
|
|
405
|
+
const ctx = workHandlerCtx.ctx;
|
|
406
|
+
if (!ctx) return { fatal: true };
|
|
407
|
+
const summary = String(args?.summary ?? "").trim();
|
|
408
|
+
if (!summary) return { fatal: false, detail: "缺少 summary" };
|
|
409
|
+
const progress = normalizeProgress(args?.progress);
|
|
410
|
+
ctx.onStatusUpdate(summary, progress);
|
|
411
|
+
return { success: true };
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const WORK_TOOLS: AITool[] = [
|
|
416
|
+
WORK_START_MOTION,
|
|
417
|
+
WORK_STOP_MOTION,
|
|
418
|
+
WORK_PERFORM_ACTION,
|
|
419
|
+
WORK_UPDATE_STATUS,
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
const WORK_TOOL_SCHEMAS = WORK_TOOLS.map((tool) => ({
|
|
423
|
+
type: "function" as const,
|
|
424
|
+
function: {
|
|
425
|
+
name: tool.name,
|
|
426
|
+
description: tool.description,
|
|
427
|
+
parameters: tool.parameters,
|
|
428
|
+
},
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
export function buildWorkSessionTools(): SessionToolDefinition[] {
|
|
432
|
+
return WORK_TOOLS.map((tool) => ({ name: tool.name, tool }));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function runWorkToolCall(call: {
|
|
436
|
+
name: string;
|
|
437
|
+
arguments: string;
|
|
438
|
+
id?: string;
|
|
439
|
+
}): Promise<{ fatal: boolean; outputText?: string }> {
|
|
440
|
+
let parsed: unknown;
|
|
441
|
+
try {
|
|
442
|
+
parsed = JSON.parse(call.arguments || "{}");
|
|
443
|
+
} catch {
|
|
444
|
+
return { fatal: false };
|
|
445
|
+
}
|
|
446
|
+
const tool = WORK_TOOLS.find((t) => t.name === call.name);
|
|
447
|
+
if (!tool) return { fatal: false };
|
|
448
|
+
try {
|
|
449
|
+
const result = await tool.handler(parsed);
|
|
450
|
+
if (call.name === "update_status" && result && typeof result === "object") {
|
|
451
|
+
const r = result as { summary?: string };
|
|
452
|
+
if (typeof r.summary === "string") return { fatal: false, outputText: r.summary };
|
|
453
|
+
}
|
|
454
|
+
return { fatal: false };
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return { fatal: false };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function normalizeProgress(input: unknown):
|
|
461
|
+
| { current: number; target: number; unit: string }
|
|
462
|
+
| undefined {
|
|
463
|
+
if (!input || typeof input !== "object") return undefined;
|
|
464
|
+
const r = input as Record<string, unknown>;
|
|
465
|
+
const current = Number(r.current);
|
|
466
|
+
const target = Number(r.target);
|
|
467
|
+
const unit = typeof r.unit === "string" ? r.unit.trim() : "";
|
|
468
|
+
if (!Number.isFinite(current) || !Number.isFinite(target) || target <= 0 || !unit) {
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
return { current, target, unit };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isWorkEvent(event: PlayEvent): boolean {
|
|
475
|
+
return !["game_chat", "qq_chat", "chat_scan_due"].includes(event.type);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function countInventoryItem(bot: any, itemName: string): number {
|
|
479
|
+
let total = 0;
|
|
480
|
+
for (const item of bot.inventory?.items?.() ?? []) {
|
|
481
|
+
const name = String(item.name ?? "").replace(/^minecraft:/, "");
|
|
482
|
+
if (name === itemName || name === `minecraft:${itemName}`) {
|
|
483
|
+
total += Number(item.count ?? 0);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return total;
|
|
487
|
+
}
|