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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
CATEGORY_PRIORITY,
|
|
4
|
+
type Behavior,
|
|
5
|
+
} from "../../behavior/base-behavior";
|
|
6
|
+
import { GatherResourceBehavior } from "../../behavior/catalog/gather";
|
|
7
|
+
import type {
|
|
8
|
+
BehaviorBundle,
|
|
9
|
+
BehaviorEntry,
|
|
10
|
+
BundleBuildContext,
|
|
11
|
+
} from "../registry";
|
|
12
|
+
|
|
13
|
+
const paramsSchema = z.object({
|
|
14
|
+
resource: z
|
|
15
|
+
.enum(["wood", "stone", "coal", "iron", "dirt"])
|
|
16
|
+
.default("wood")
|
|
17
|
+
.describe("资源类型"),
|
|
18
|
+
count: z.number().int().min(1).max(64).default(1).describe("目标采集数量"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type GatherResourceParams = z.infer<typeof paramsSchema>;
|
|
22
|
+
|
|
23
|
+
export const gatherResourceBundle: BehaviorBundle<GatherResourceParams> = {
|
|
24
|
+
id: "task.gather_resource",
|
|
25
|
+
description: "采集资源(wood/stone/coal/iron/dirt)。",
|
|
26
|
+
mode: "MISSION",
|
|
27
|
+
paramsSchema,
|
|
28
|
+
build(params: GatherResourceParams): BehaviorEntry[] {
|
|
29
|
+
const b: Behavior = new GatherResourceBehavior();
|
|
30
|
+
b.configure({ resource: params.resource, count: String(params.count) });
|
|
31
|
+
return [{ priority: CATEGORY_PRIORITY.movement, behavior: b }];
|
|
32
|
+
},
|
|
33
|
+
snapshot(internal: unknown): unknown {
|
|
34
|
+
const i = internal as { resource: string } | null;
|
|
35
|
+
return i ? { resource: i.resource } : null;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CATEGORY_PRIORITY,
|
|
3
|
+
type Behavior,
|
|
4
|
+
} from "../../behavior/base-behavior";
|
|
5
|
+
import { IdleWanderBehavior } from "../../behavior/catalog/idle";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import type {
|
|
8
|
+
BehaviorBundle,
|
|
9
|
+
BehaviorEntry,
|
|
10
|
+
} from "../registry";
|
|
11
|
+
|
|
12
|
+
const paramsSchema = z.object({}).strict();
|
|
13
|
+
|
|
14
|
+
export const idleWanderBundle: BehaviorBundle<Record<string, never>> = {
|
|
15
|
+
id: "task.idle_wander",
|
|
16
|
+
description: "原地待机:随机张望 + 偶尔短走。最低优先级。",
|
|
17
|
+
mode: "IDLE",
|
|
18
|
+
paramsSchema,
|
|
19
|
+
build(): BehaviorEntry[] {
|
|
20
|
+
const b: Behavior = new IdleWanderBehavior();
|
|
21
|
+
b.configure({});
|
|
22
|
+
return [{ priority: CATEGORY_PRIORITY.movement, behavior: b }];
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CATEGORY_PRIORITY, type Behavior } from "../../behavior/base-behavior";
|
|
3
|
+
import { SeekShelterBehavior } from "../../behavior/catalog/seek-shelter";
|
|
4
|
+
import type { BehaviorBundle, BehaviorEntry } from "../registry";
|
|
5
|
+
|
|
6
|
+
const paramsSchema = z.object({}).strict();
|
|
7
|
+
|
|
8
|
+
export const seekShelterBundle: BehaviorBundle<Record<string, never>> = {
|
|
9
|
+
id: "task.seek_shelter",
|
|
10
|
+
description: "寻找附近有实体屋顶的安全位置,到达后完成。",
|
|
11
|
+
mode: "MISSION",
|
|
12
|
+
paramsSchema,
|
|
13
|
+
build(): BehaviorEntry[] {
|
|
14
|
+
const behavior: Behavior = new SeekShelterBehavior();
|
|
15
|
+
behavior.configure({});
|
|
16
|
+
return [{ priority: CATEGORY_PRIORITY.movement, behavior }];
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { BehaviorMissionReporter } from "../behavior/base-behavior";
|
|
3
|
+
import type { BehaviorEngine } from "../behavior/engine";
|
|
4
|
+
import type { MemoryBus } from "../state/memory-bus";
|
|
5
|
+
import type {
|
|
6
|
+
MissionErrorCode,
|
|
7
|
+
MissionOutcome,
|
|
8
|
+
MissionState,
|
|
9
|
+
ModeState,
|
|
10
|
+
} from "../state/mode";
|
|
11
|
+
import type { BehaviorBundle, TaskRegistry } from "./registry";
|
|
12
|
+
|
|
13
|
+
export type BundleId = string;
|
|
14
|
+
export type MissionId = string;
|
|
15
|
+
|
|
16
|
+
export type SwitchResult =
|
|
17
|
+
| {
|
|
18
|
+
kind: "applied";
|
|
19
|
+
missionId: MissionId;
|
|
20
|
+
bundleId: BundleId;
|
|
21
|
+
effectiveAt: number;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
kind: "rejected";
|
|
26
|
+
reason:
|
|
27
|
+
| "unknown_bundle"
|
|
28
|
+
| "invalid_params"
|
|
29
|
+
| "no_bot_session"
|
|
30
|
+
| "engine_busy"
|
|
31
|
+
| "rejected_by_engine";
|
|
32
|
+
detail: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface MissionSpec {
|
|
36
|
+
bundle: BundleId;
|
|
37
|
+
params?: Record<string, unknown>;
|
|
38
|
+
objective?: string;
|
|
39
|
+
directiveId?: string;
|
|
40
|
+
completesDirectiveOnSuccess?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MissionControllerOptions {
|
|
44
|
+
registry: TaskRegistry;
|
|
45
|
+
engine: BehaviorEngine;
|
|
46
|
+
bus: MemoryBus;
|
|
47
|
+
buildContext: () => any;
|
|
48
|
+
log?: (msg: string) => void;
|
|
49
|
+
onOutcome?: (outcome: MissionOutcome) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const EMPTY_LAST_SWITCH = {
|
|
53
|
+
from: null,
|
|
54
|
+
to: "IDLE" as const,
|
|
55
|
+
reason: "init",
|
|
56
|
+
at: 0,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export class MissionController {
|
|
60
|
+
private current: MissionState | null = null;
|
|
61
|
+
private currentBundle: BehaviorBundle | null = null;
|
|
62
|
+
private lastOutcome: MissionOutcome | null = null;
|
|
63
|
+
private modeState: ModeState = {
|
|
64
|
+
current: "IDLE",
|
|
65
|
+
mission: null,
|
|
66
|
+
lastSwitch: { ...EMPTY_LAST_SWITCH, at: Date.now() },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
constructor(private readonly opts: MissionControllerOptions) {}
|
|
70
|
+
|
|
71
|
+
getModeState(): ModeState {
|
|
72
|
+
return this.modeState;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getCurrentMission(): MissionState | null {
|
|
76
|
+
return this.current ? { ...this.current } : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getLastOutcome(): MissionOutcome | null {
|
|
80
|
+
return this.lastOutcome ? { ...this.lastOutcome } : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
startMission(spec: MissionSpec): SwitchResult {
|
|
84
|
+
const validation = this.opts.registry.validate(spec.bundle, spec.params);
|
|
85
|
+
if (!validation.ok) {
|
|
86
|
+
const reason = validation.error.startsWith("unknown_bundle")
|
|
87
|
+
? "unknown_bundle"
|
|
88
|
+
: "invalid_params";
|
|
89
|
+
return { kind: "rejected", reason, detail: validation.error };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const bundle = this.opts.registry.get(spec.bundle);
|
|
93
|
+
if (!bundle) {
|
|
94
|
+
return {
|
|
95
|
+
kind: "rejected",
|
|
96
|
+
reason: "unknown_bundle",
|
|
97
|
+
detail: `bundle not found: ${spec.bundle}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ctx = this.opts.buildContext();
|
|
102
|
+
if (!ctx?.bot) {
|
|
103
|
+
return {
|
|
104
|
+
kind: "rejected",
|
|
105
|
+
reason: "no_bot_session",
|
|
106
|
+
detail: "bot 尚未连接或上下文不可用",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const missionId = randomUUID();
|
|
111
|
+
const startedAt = Date.now();
|
|
112
|
+
const params = validation.params as Record<string, unknown>;
|
|
113
|
+
const state: MissionState = {
|
|
114
|
+
missionId,
|
|
115
|
+
bundleId: bundle.id,
|
|
116
|
+
params,
|
|
117
|
+
startedAt,
|
|
118
|
+
status: "running",
|
|
119
|
+
progress: bundle.snapshot ? bundle.snapshot(params) : params,
|
|
120
|
+
objective: spec.objective,
|
|
121
|
+
directiveId: spec.directiveId,
|
|
122
|
+
completesDirectiveOnSuccess: spec.completesDirectiveOnSuccess ?? true,
|
|
123
|
+
};
|
|
124
|
+
const reporter = this.createReporter(missionId);
|
|
125
|
+
const built = safeBuild(
|
|
126
|
+
bundle,
|
|
127
|
+
state.params,
|
|
128
|
+
{ ...ctx, bus: this.opts.bus, mission: reporter },
|
|
129
|
+
this.opts.log,
|
|
130
|
+
);
|
|
131
|
+
if (!built.ok) {
|
|
132
|
+
return {
|
|
133
|
+
kind: "rejected",
|
|
134
|
+
reason: "rejected_by_engine",
|
|
135
|
+
detail: built.error,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this.current)
|
|
140
|
+
this.finish(
|
|
141
|
+
this.current.missionId,
|
|
142
|
+
"cancelled",
|
|
143
|
+
"cancelled",
|
|
144
|
+
"任务被新任务替换",
|
|
145
|
+
);
|
|
146
|
+
for (const entry of built.entries) entry.behavior.bindMission(reporter);
|
|
147
|
+
this.current = state;
|
|
148
|
+
this.currentBundle = bundle;
|
|
149
|
+
this.opts.engine.setMissionBehaviors(
|
|
150
|
+
missionId,
|
|
151
|
+
built.entries.map((entry) => entry.behavior),
|
|
152
|
+
);
|
|
153
|
+
this.updateBus();
|
|
154
|
+
this.recordModeSwitch(bundle.mode ?? "MISSION", `mission:${bundle.id}`);
|
|
155
|
+
this.opts.log?.(
|
|
156
|
+
`[MC/play] 任务启动: ${bundle.id} (${missionId.slice(0, 8)})`,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
kind: "applied",
|
|
161
|
+
missionId,
|
|
162
|
+
bundleId: bundle.id,
|
|
163
|
+
effectiveAt: startedAt,
|
|
164
|
+
message: `已启动 ${bundle.id}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
tick(): void {
|
|
169
|
+
const current = this.current;
|
|
170
|
+
const bundle = this.currentBundle;
|
|
171
|
+
if (!current || typeof bundle?.isFinished !== "function") return;
|
|
172
|
+
const ctx = this.opts.buildContext();
|
|
173
|
+
if (!ctx?.bot) return;
|
|
174
|
+
try {
|
|
175
|
+
if (
|
|
176
|
+
bundle.isFinished({
|
|
177
|
+
...ctx,
|
|
178
|
+
bus: this.opts.bus,
|
|
179
|
+
mission: this.createReporter(current.missionId),
|
|
180
|
+
startedAt: current.startedAt,
|
|
181
|
+
internal: current.progress,
|
|
182
|
+
})
|
|
183
|
+
) {
|
|
184
|
+
this.finish(current.missionId, "succeeded", undefined, "任务完成");
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.opts.log?.(
|
|
188
|
+
`[MC/play] mission.isFinished 抛错 (${bundle.id}),强制结束: ${error}`,
|
|
189
|
+
);
|
|
190
|
+
this.finish(
|
|
191
|
+
current.missionId,
|
|
192
|
+
"failed",
|
|
193
|
+
"unknown",
|
|
194
|
+
`isFinished threw: ${String(error)}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
stopMission(reason = "manual_stop"): SwitchResult {
|
|
200
|
+
const current = this.current;
|
|
201
|
+
if (!current) {
|
|
202
|
+
return {
|
|
203
|
+
kind: "rejected",
|
|
204
|
+
reason: "rejected_by_engine",
|
|
205
|
+
detail: "没有进行中的任务",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
this.finish(current.missionId, "cancelled", "cancelled", reason);
|
|
209
|
+
return {
|
|
210
|
+
kind: "applied",
|
|
211
|
+
missionId: current.missionId,
|
|
212
|
+
bundleId: current.bundleId,
|
|
213
|
+
effectiveAt: Date.now(),
|
|
214
|
+
message: `已停止 ${current.bundleId}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private createReporter(missionId: string): BehaviorMissionReporter {
|
|
219
|
+
return {
|
|
220
|
+
progress: (value) => {
|
|
221
|
+
if (this.current?.missionId !== missionId) return;
|
|
222
|
+
this.current.progress = value;
|
|
223
|
+
this.updateBus();
|
|
224
|
+
},
|
|
225
|
+
succeed: (detail, progress) =>
|
|
226
|
+
this.finish(missionId, "succeeded", undefined, detail, progress),
|
|
227
|
+
fail: (code, detail, progress) =>
|
|
228
|
+
this.finish(missionId, "failed", code, detail, progress),
|
|
229
|
+
block: (code, detail, progress) =>
|
|
230
|
+
this.finish(missionId, "blocked", code, detail, progress),
|
|
231
|
+
isCurrent: () => this.current?.missionId === missionId,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private finish(
|
|
236
|
+
missionId: string,
|
|
237
|
+
status: MissionOutcome["status"],
|
|
238
|
+
code?: MissionErrorCode,
|
|
239
|
+
detail?: string,
|
|
240
|
+
progress?: unknown,
|
|
241
|
+
): void {
|
|
242
|
+
const current = this.current;
|
|
243
|
+
if (!current || current.missionId !== missionId) return;
|
|
244
|
+
this.opts.engine.removeMissionBehaviors(missionId);
|
|
245
|
+
const outcome: MissionOutcome = {
|
|
246
|
+
missionId,
|
|
247
|
+
bundleId: current.bundleId,
|
|
248
|
+
status,
|
|
249
|
+
code,
|
|
250
|
+
detail,
|
|
251
|
+
progress: progress ?? current.progress,
|
|
252
|
+
startedAt: current.startedAt,
|
|
253
|
+
endedAt: Date.now(),
|
|
254
|
+
directiveId: current.directiveId,
|
|
255
|
+
completesDirectiveOnSuccess: current.completesDirectiveOnSuccess,
|
|
256
|
+
};
|
|
257
|
+
this.current = null;
|
|
258
|
+
this.currentBundle = null;
|
|
259
|
+
this.lastOutcome = outcome;
|
|
260
|
+
this.updateBus();
|
|
261
|
+
this.recordModeSwitch("IDLE", `mission_${status}`);
|
|
262
|
+
this.opts.log?.(
|
|
263
|
+
`[MC/play] 任务结束: ${outcome.bundleId} status=${status}${code ? ` code=${code}` : ""}`,
|
|
264
|
+
);
|
|
265
|
+
this.opts.onOutcome?.(outcome);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private updateBus(): void {
|
|
269
|
+
this.opts.bus.set("mission", this.current, { ttlMs: 0 });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private recordModeSwitch(to: ModeState["current"], reason: string): void {
|
|
273
|
+
const from = this.modeState.current;
|
|
274
|
+
this.modeState = {
|
|
275
|
+
current: to,
|
|
276
|
+
mission: this.current,
|
|
277
|
+
lastSwitch: { from, to, reason, at: Date.now() },
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function safeBuild(
|
|
283
|
+
bundle: BehaviorBundle,
|
|
284
|
+
params: unknown,
|
|
285
|
+
ctx: any,
|
|
286
|
+
log?: (msg: string) => void,
|
|
287
|
+
):
|
|
288
|
+
| { ok: true; entries: ReturnType<BehaviorBundle["build"]> }
|
|
289
|
+
| { ok: false; error: string } {
|
|
290
|
+
try {
|
|
291
|
+
return { ok: true, entries: (bundle.build as any)(params, ctx) };
|
|
292
|
+
} catch (error) {
|
|
293
|
+
log?.(`[MC/play] bundle.build 失败 (${bundle.id}): ${error}`);
|
|
294
|
+
return { ok: false, error: String(error) };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Behavior, BehaviorMissionReporter } from "../behavior/base-behavior";
|
|
3
|
+
import type { BehaviorMode } from "../state/mode";
|
|
4
|
+
|
|
5
|
+
export interface BehaviorEntry {
|
|
6
|
+
priority: number;
|
|
7
|
+
behavior: Behavior;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BundleBuildContext {
|
|
11
|
+
bot: any;
|
|
12
|
+
movements: any;
|
|
13
|
+
log: (msg: string) => void;
|
|
14
|
+
bus: any;
|
|
15
|
+
mission: BehaviorMissionReporter;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MissionContext extends BundleBuildContext {
|
|
19
|
+
startedAt: number;
|
|
20
|
+
internal: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BehaviorBundle<P = Record<string, unknown>> {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly description: string;
|
|
26
|
+
readonly paramsSchema: z.ZodType<P, any, any>;
|
|
27
|
+
readonly mode: BehaviorMode | null;
|
|
28
|
+
build(params: P, ctx: BundleBuildContext): BehaviorEntry[];
|
|
29
|
+
isFinished?(ctx: MissionContext): boolean;
|
|
30
|
+
snapshot?(internal: unknown): unknown;
|
|
31
|
+
serialize?(): unknown;
|
|
32
|
+
deserialize?(data: unknown): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type BundleId = string;
|
|
36
|
+
|
|
37
|
+
export type ValidateResult =
|
|
38
|
+
| { ok: true; params: unknown }
|
|
39
|
+
| { ok: false; error: string };
|
|
40
|
+
|
|
41
|
+
export interface BundleListEntry {
|
|
42
|
+
id: string;
|
|
43
|
+
description: string;
|
|
44
|
+
mode: BehaviorMode | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class TaskRegistry {
|
|
48
|
+
private bundles = new Map<BundleId, BehaviorBundle>();
|
|
49
|
+
|
|
50
|
+
register<P>(bundle: BehaviorBundle<P>): void {
|
|
51
|
+
this.bundles.set(bundle.id, bundle as unknown as BehaviorBundle);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get(id: string): BehaviorBundle | undefined {
|
|
55
|
+
return this.bundles.get(id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
list(): BundleListEntry[] {
|
|
59
|
+
return [...this.bundles.values()].map((b) => ({
|
|
60
|
+
id: b.id,
|
|
61
|
+
description: b.description,
|
|
62
|
+
mode: b.mode,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe(): Array<BundleListEntry & { params: unknown }> {
|
|
67
|
+
return [...this.bundles.values()]
|
|
68
|
+
.map((bundle) => ({
|
|
69
|
+
id: bundle.id,
|
|
70
|
+
description: bundle.description,
|
|
71
|
+
mode: bundle.mode,
|
|
72
|
+
params: describeSchema(bundle.paramsSchema),
|
|
73
|
+
}))
|
|
74
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
validate(id: string, params: unknown): ValidateResult {
|
|
78
|
+
const bundle = this.bundles.get(id);
|
|
79
|
+
if (!bundle) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: `unknown_bundle: ${id} (available: ${this.list().map((b) => b.id).join(", ") || "none"})`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const result = bundle.paramsSchema.safeParse(params ?? {});
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: `invalid_params: ${result.error.issues
|
|
90
|
+
.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
|
|
91
|
+
.join("; ")}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, params: result.data };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function describeSchema(schema: z.ZodTypeAny): unknown {
|
|
99
|
+
const definition = (schema as any)?._def;
|
|
100
|
+
if (definition?.typeName !== z.ZodFirstPartyTypeKind.ZodObject) return {};
|
|
101
|
+
const shape = definition.shape();
|
|
102
|
+
const out: Record<string, string> = {};
|
|
103
|
+
for (const key of Object.keys(shape).sort()) {
|
|
104
|
+
out[key] = String(shape[key]?._def?.description ?? shape[key]?._def?.typeName ?? "unknown");
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|