@volc-emr/emr-cli 0.1.0-beta.0 → 0.1.0-beta.2

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.
Files changed (42) hide show
  1. package/README.md +81 -269
  2. package/dist/agent/agent.js +14 -3
  3. package/dist/agent/executor.js +55 -8
  4. package/dist/agent/llmPlanner.js +4 -17
  5. package/dist/commands/config.js +57 -0
  6. package/dist/commands/init.js +102 -0
  7. package/dist/commands/run.js +41 -0
  8. package/dist/core/agent.js +30 -0
  9. package/dist/core/executor.js +36 -0
  10. package/dist/core/llm-planner.js +131 -0
  11. package/dist/core/planner.js +12 -0
  12. package/dist/core/registry.js +11 -0
  13. package/dist/core/tool.js +2 -0
  14. package/dist/core/types.js +2 -0
  15. package/dist/index.js +8 -174
  16. package/dist/integrations/volc/client.js +53 -0
  17. package/dist/integrations/volc/createClusterMemory.js +56 -0
  18. package/dist/integrations/volc/emr.js +177 -0
  19. package/dist/integrations/volc/tools/createCluster.js +335 -0
  20. package/dist/integrations/volc/tools/deleteCluster.js +15 -0
  21. package/dist/integrations/volc/tools/findClustersToCleanup.js +18 -0
  22. package/dist/integrations/volc/tools/index.js +15 -0
  23. package/dist/integrations/volc/tools/listClusters.js +68 -0
  24. package/dist/runtime/logger.js +118 -4
  25. package/dist/services/ecsApi.js +159 -0
  26. package/dist/services/emrApi.js +0 -22
  27. package/dist/shared/config.js +73 -0
  28. package/dist/shared/confirm.js +92 -0
  29. package/dist/shared/llm.js +64 -0
  30. package/dist/shared/logger.js +122 -0
  31. package/dist/shared/memory.js +4 -0
  32. package/dist/shared/prompt.js +9 -0
  33. package/dist/tools/ecs/createCluster.js +335 -0
  34. package/dist/tools/ecs/deleteCluster.js +127 -0
  35. package/dist/tools/ecs/findClustersToCleanup.js +32 -0
  36. package/dist/tools/ecs/index.js +13 -0
  37. package/dist/tools/ecs/listClusters.js +68 -0
  38. package/dist/tools/emr/deleteCluster.js +12 -3
  39. package/dist/tools/emr/findClustersToCleanup.js +16 -2
  40. package/dist/tools/emr/index.js +0 -2
  41. package/dist/tools/registry.js +3 -3
  42. package/package.json +2 -2
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.systemPrompt = void 0;
4
+ exports.systemPrompt = `
5
+ You are a CLI Agent planner.
6
+ Given a natural language task and a list of tools,
7
+ output a JSON array of { tool, input } steps.
8
+ Never call tools yourself. Only produce plans.
9
+ `.trim();
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCluster = void 0;
4
+ const zod_1 = require("zod");
5
+ const confirm_1 = require("../../runtime/confirm");
6
+ const createClusterMemory_1 = require("../../runtime/createClusterMemory");
7
+ const CLUSTER_TYPES = [
8
+ "Hadoop",
9
+ "Presto",
10
+ "Trino",
11
+ "Stream-Kafka",
12
+ "Stream-Flink",
13
+ "HBase",
14
+ "OpenSearch",
15
+ "StarRocks",
16
+ "TensorFlow",
17
+ "Doris",
18
+ "Pulsar",
19
+ "ClickHouse",
20
+ "ZooKeeper"
21
+ ];
22
+ const CHARGE_TYPES = ["POST", "PRE"];
23
+ const DEPLOY_MODES = ["SIMPLE", "HIGH_AVAILABLE"];
24
+ const SECURITY_MODES = ["SIMPLE", "KERBEROS"];
25
+ const HISTORY_SERVER_MODES = ["LOCAL", "PHS"];
26
+ const NODE_GROUP_TYPES = ["MASTER", "CORE", "TASK", "GATEWAY"];
27
+ const SystemDiskSchema = zod_1.z.object({
28
+ VolumeType: zod_1.z.string().default("ESSD_FlexPL"),
29
+ Size: zod_1.z.number().int().positive().default(80)
30
+ });
31
+ const DataDiskSchema = zod_1.z.object({
32
+ VolumeType: zod_1.z.string(),
33
+ Size: zod_1.z.number().int().positive(),
34
+ Count: zod_1.z.number().int().positive()
35
+ });
36
+ const NodeGroupSchema = zod_1.z.object({
37
+ NodeGroupType: zod_1.z.enum(NODE_GROUP_TYPES),
38
+ NodeGroupName: zod_1.z.string().optional(),
39
+ NodeCount: zod_1.z.number().int().positive(),
40
+ ZoneId: zod_1.z.string().optional(),
41
+ SubnetIds: zod_1.z.array(zod_1.z.string()).optional(),
42
+ EcsInstanceTypes: zod_1.z.array(zod_1.z.string()).optional(),
43
+ SystemDisk: SystemDiskSchema.optional(),
44
+ DataDisks: zod_1.z.array(DataDiskSchema).optional(),
45
+ EcsKeyPairName: zod_1.z.string().optional(),
46
+ EcsPassword: zod_1.z.string().optional(),
47
+ Bandwidth: zod_1.z.number().int().nonnegative().optional(),
48
+ ChargeType: zod_1.z.enum(CHARGE_TYPES).optional(),
49
+ WithPublicIp: zod_1.z.boolean().optional()
50
+ });
51
+ const ChargePreConfigSchema = zod_1.z.object({
52
+ ChargePeriodUnit: zod_1.z.enum(["Month", "Year"]).default("Month"),
53
+ ChargePeriod: zod_1.z.number().int().positive().default(1),
54
+ AutoRenew: zod_1.z.boolean().default(false),
55
+ AutoRenewPeriodUnit: zod_1.z.enum(["Month", "Year"]).optional(),
56
+ AutoRenewPeriod: zod_1.z.number().int().positive().optional()
57
+ });
58
+ function randomSuffix() {
59
+ return Math.random().toString(36).slice(2, 7);
60
+ }
61
+ function isMissing(v) {
62
+ if (v === undefined || v === null)
63
+ return true;
64
+ if (typeof v === "string" && !v.trim())
65
+ return true;
66
+ if (Array.isArray(v) && v.length === 0)
67
+ return true;
68
+ return false;
69
+ }
70
+ function parseList(input) {
71
+ return input
72
+ .split(/[,,\s]+/)
73
+ .map((s) => s.trim())
74
+ .filter(Boolean);
75
+ }
76
+ function parseBool(input, defaultValue = false) {
77
+ const v = input.trim().toLowerCase();
78
+ if (!v)
79
+ return defaultValue;
80
+ return ["y", "yes", "true", "1", "是"].includes(v);
81
+ }
82
+ function parseInt10(input, defaultValue) {
83
+ const n = parseInt(input, 10);
84
+ if (Number.isFinite(n))
85
+ return n;
86
+ return defaultValue;
87
+ }
88
+ async function askEnum(prompt, question, enumValues, current, defaultValue) {
89
+ if (current && enumValues.includes(current))
90
+ return current;
91
+ const def = defaultValue && enumValues.includes(defaultValue) ? defaultValue : undefined;
92
+ const label = `${question} (可选: ${enumValues.join(" / ")})`;
93
+ while (true) {
94
+ const answer = (await prompt.ask(label, { defaultValue: def }));
95
+ if (enumValues.includes(answer))
96
+ return answer;
97
+ process.stdout.write(` 非法取值,请从 ${enumValues.join(" / ")} 中选择\n`);
98
+ }
99
+ }
100
+ async function askRequired(prompt, question, current, defaultValue) {
101
+ if (current && current.trim())
102
+ return current;
103
+ while (true) {
104
+ const v = await prompt.ask(question, { defaultValue });
105
+ if (v && v.trim())
106
+ return v.trim();
107
+ process.stdout.write(" 该字段必填,请输入\n");
108
+ }
109
+ }
110
+ async function completeNodeGroup(prompt, ng, defaults) {
111
+ const mem = defaults.memory || {};
112
+ const type = await askEnum(prompt, `节点组类型 (当前: ${ng.NodeGroupType || "未设置"})`, NODE_GROUP_TYPES, ng.NodeGroupType);
113
+ const name = ng.NodeGroupName ||
114
+ `OpenApi-${type[0] + type.slice(1).toLowerCase()}Group-${randomSuffix()}`;
115
+ const count = ng.NodeCount ??
116
+ parseInt10(await prompt.ask(` 节点数`, {
117
+ defaultValue: type === "MASTER" ? "3" : type === "CORE" ? "3" : "1"
118
+ }), type === "MASTER" ? 3 : type === "CORE" ? 3 : 1);
119
+ let subnets = ng.SubnetIds;
120
+ if (isMissing(subnets)) {
121
+ const memDefault = mem.SubnetIds && mem.SubnetIds.length ? mem.SubnetIds.join(",") : undefined;
122
+ const raw = memDefault
123
+ ? await prompt.ask(` SubnetIds (逗号分隔, 至少 1 个)`, {
124
+ defaultValue: memDefault
125
+ })
126
+ : await askRequired(prompt, ` SubnetIds (逗号分隔, 至少 1 个)`);
127
+ subnets = parseList(raw);
128
+ if (!subnets.length) {
129
+ subnets = await askRequired(prompt, ` SubnetIds 不能为空,请重新输入`).then(parseList);
130
+ }
131
+ }
132
+ let instanceTypes = ng.EcsInstanceTypes;
133
+ if (isMissing(instanceTypes)) {
134
+ const memDefault = mem.EcsInstanceTypes && mem.EcsInstanceTypes.length
135
+ ? mem.EcsInstanceTypes.join(",")
136
+ : "ecs.g3i.2xlarge";
137
+ const raw = await prompt.ask(` EcsInstanceTypes (逗号分隔)`, {
138
+ defaultValue: memDefault
139
+ });
140
+ instanceTypes = parseList(raw);
141
+ if (!instanceTypes.length)
142
+ instanceTypes = ["ecs.g3i.2xlarge"];
143
+ }
144
+ const system = ng.SystemDisk || { VolumeType: "ESSD_FlexPL", Size: 80 };
145
+ const keyPair = ng.EcsKeyPairName ||
146
+ (await prompt.ask(` EcsKeyPairName (可选, 回车跳过)`, {
147
+ defaultValue: mem.EcsKeyPairName
148
+ })) ||
149
+ undefined;
150
+ return {
151
+ NodeGroupType: type,
152
+ NodeGroupName: name,
153
+ NodeCount: count,
154
+ ZoneId: ng.ZoneId || defaults.ZoneId,
155
+ SubnetIds: subnets,
156
+ EcsInstanceTypes: instanceTypes,
157
+ SystemDisk: system,
158
+ DataDisks: ng.DataDisks,
159
+ EcsKeyPairName: keyPair,
160
+ EcsPassword: ng.EcsPassword,
161
+ Bandwidth: ng.Bandwidth,
162
+ ChargeType: ng.ChargeType || defaults.ChargeType,
163
+ WithPublicIp: ng.WithPublicIp ?? false
164
+ };
165
+ }
166
+ exports.createCluster = {
167
+ name: "createCluster",
168
+ description: "Create a Volcengine EMR cluster (CreateCluster). The tool will interactively ask the user for any missing required fields, " +
169
+ "then show a summary and ask for final confirmation before calling the OpenAPI. " +
170
+ "LLM should pre-fill as many fields as it can infer from the user's natural language description (e.g. cluster type, version, node count, charge type), " +
171
+ "and leave unknown fields as undefined — the tool itself will prompt the user.",
172
+ riskLevel: "high",
173
+ input: zod_1.z.object({
174
+ ProjectName: zod_1.z.string().optional(),
175
+ ClusterName: zod_1.z.string().optional(),
176
+ ClusterType: zod_1.z.enum(CLUSTER_TYPES).optional(),
177
+ ReleaseVersion: zod_1.z.string().optional(),
178
+ DeployMode: zod_1.z.enum(DEPLOY_MODES).optional(),
179
+ SecurityMode: zod_1.z.enum(SECURITY_MODES).optional(),
180
+ HistoryServerMode: zod_1.z.enum(HISTORY_SERVER_MODES).optional(),
181
+ ChargeType: zod_1.z.enum(CHARGE_TYPES).optional(),
182
+ ChargePreConfig: ChargePreConfigSchema.partial().optional(),
183
+ VpcId: zod_1.z.string().optional(),
184
+ SecurityGroupId: zod_1.z.string().optional(),
185
+ ZoneId: zod_1.z.string().optional(),
186
+ EcsIamRole: zod_1.z.string().optional(),
187
+ NodeGroupAttributes: zod_1.z.array(NodeGroupSchema.partial()).optional(),
188
+ ApplicationNames: zod_1.z.array(zod_1.z.string()).optional()
189
+ }),
190
+ async execute(input, ctx) {
191
+ const prompt = (0, confirm_1.createPromptSession)();
192
+ const memory = (0, createClusterMemory_1.readCreateClusterMemory)();
193
+ const pick = (inputVal, memVal, fallback) => {
194
+ if (inputVal !== undefined && inputVal !== null && inputVal !== "")
195
+ return inputVal;
196
+ if (memVal !== undefined && memVal !== null && memVal !== "")
197
+ return memVal;
198
+ return fallback;
199
+ };
200
+ try {
201
+ process.stdout.write("\n=== 创建 EMR 集群:补全配置 ===\n");
202
+ if (memory.updatedAt) {
203
+ process.stdout.write(`(已加载上次记忆,路径: ${(0, createClusterMemory_1.createClusterMemoryPath)()})\n`);
204
+ }
205
+ const ClusterType = await askEnum(prompt, "集群类型", CLUSTER_TYPES, input.ClusterType, memory.ClusterType || "Hadoop");
206
+ const ReleaseVersion = await askRequired(prompt, "集群版本 (ReleaseVersion, 如 3.7.0)", input.ReleaseVersion, memory.ReleaseVersion || "3.7.0");
207
+ const ClusterName = await askRequired(prompt, "集群名称", input.ClusterName, `OpenApi${ClusterType}${ReleaseVersion}-${randomSuffix()}`);
208
+ const ChargeType = await askEnum(prompt, "付费类型", CHARGE_TYPES, input.ChargeType, memory.ChargeType || "POST");
209
+ let ChargePreConfig;
210
+ if (ChargeType === "PRE") {
211
+ const pre = input.ChargePreConfig || {};
212
+ const unit = await askEnum(prompt, " 包年包月计费单位", ["Month", "Year"], pre.ChargePeriodUnit, "Month");
213
+ const period = parseInt10(await prompt.ask(" 包年包月周期数", {
214
+ defaultValue: String(pre.ChargePeriod ?? 1)
215
+ }), pre.ChargePeriod ?? 1);
216
+ const autoRenew = parseBool(await prompt.ask(" 是否自动续费 (y/n)", {
217
+ defaultValue: pre.AutoRenew ? "y" : "n"
218
+ }), !!pre.AutoRenew);
219
+ ChargePreConfig = {
220
+ ChargeType: "PRE",
221
+ ChargePeriodUnit: unit,
222
+ ChargePeriod: period,
223
+ AutoRenew: autoRenew,
224
+ AutoRenewPeriodUnit: autoRenew ? unit : undefined,
225
+ AutoRenewPeriod: autoRenew ? period : undefined
226
+ };
227
+ }
228
+ const DeployMode = await askEnum(prompt, "部署模式", DEPLOY_MODES, input.DeployMode, memory.DeployMode || "HIGH_AVAILABLE");
229
+ const SecurityMode = await askEnum(prompt, "安全模式", SECURITY_MODES, input.SecurityMode, memory.SecurityMode || "SIMPLE");
230
+ const VpcId = await askRequired(prompt, "VpcId (如 vpc-xxx)", input.VpcId, memory.VpcId);
231
+ const SecurityGroupId = await askRequired(prompt, "SecurityGroupId (如 sg-xxx)", input.SecurityGroupId, memory.SecurityGroupId);
232
+ const ZoneId = await askRequired(prompt, "可用区 ZoneId (如 cn-beijing-b)", input.ZoneId, memory.ZoneId || "cn-beijing-b");
233
+ const EcsIamRole = input.EcsIamRole ||
234
+ (await prompt.ask("EcsIamRole (可选, 回车跳过)", {
235
+ defaultValue: memory.EcsIamRole
236
+ })) ||
237
+ undefined;
238
+ let nodeGroupsInput = input.NodeGroupAttributes || [];
239
+ const hasMaster = nodeGroupsInput.some((g) => g?.NodeGroupType === "MASTER");
240
+ const hasCore = nodeGroupsInput.some((g) => g?.NodeGroupType === "CORE");
241
+ if (!hasMaster)
242
+ nodeGroupsInput = [...nodeGroupsInput, { NodeGroupType: "MASTER" }];
243
+ if (!hasCore)
244
+ nodeGroupsInput = [...nodeGroupsInput, { NodeGroupType: "CORE" }];
245
+ process.stdout.write(`\n-- 节点组配置 (共 ${nodeGroupsInput.length} 组) --\n`);
246
+ const NodeGroupAttributes = [];
247
+ for (const [i, ng] of nodeGroupsInput.entries()) {
248
+ process.stdout.write(`\n[节点组 ${i + 1}]\n`);
249
+ NodeGroupAttributes.push(await completeNodeGroup(prompt, ng, {
250
+ ZoneId,
251
+ ChargeType,
252
+ memory
253
+ }));
254
+ }
255
+ let ApplicationNames = input.ApplicationNames;
256
+ if (isMissing(ApplicationNames)) {
257
+ const raw = await prompt.ask("\nApplicationNames (逗号分隔, 回车跳过用默认组件)", {
258
+ defaultValue: memory.ApplicationNames && memory.ApplicationNames.length
259
+ ? memory.ApplicationNames.join(",")
260
+ : undefined
261
+ });
262
+ ApplicationNames = parseList(raw);
263
+ if (!ApplicationNames.length)
264
+ ApplicationNames = undefined;
265
+ }
266
+ const ProjectName = pick(input.ProjectName, memory.ProjectName, "default") || "default";
267
+ const params = {
268
+ ProjectName,
269
+ ClusterName,
270
+ ClusterType,
271
+ ReleaseVersion,
272
+ DeployMode,
273
+ SecurityMode,
274
+ HistoryServerMode: input.HistoryServerMode || "LOCAL",
275
+ ChargeType,
276
+ ChargePreConfig,
277
+ VpcId,
278
+ SecurityGroupId,
279
+ NodeAttribute: {
280
+ ZoneId,
281
+ EcsIamRole
282
+ },
283
+ NodeGroupAttributes,
284
+ ApplicationNames
285
+ };
286
+ process.stdout.write("\n=== 即将创建集群,配置如下 ===\n");
287
+ process.stdout.write(JSON.stringify(params, null, 2) + "\n\n");
288
+ const ok = await (0, confirm_1.confirm)("确认使用以上配置创建集群?");
289
+ if (!ok) {
290
+ return { Skipped: true, Reason: "USER_CANCELLED", Params: params };
291
+ }
292
+ const persistMemory = () => {
293
+ try {
294
+ const firstNg = NodeGroupAttributes[0];
295
+ (0, createClusterMemory_1.writeCreateClusterMemory)({
296
+ ProjectName,
297
+ ClusterType,
298
+ ReleaseVersion,
299
+ ChargeType,
300
+ DeployMode,
301
+ SecurityMode,
302
+ VpcId,
303
+ SecurityGroupId,
304
+ ZoneId,
305
+ EcsIamRole,
306
+ SubnetIds: firstNg?.SubnetIds,
307
+ EcsInstanceTypes: firstNg?.EcsInstanceTypes,
308
+ EcsKeyPairName: firstNg?.EcsKeyPairName,
309
+ ApplicationNames
310
+ });
311
+ process.stdout.write(`(已更新常用默认值记忆: ${(0, createClusterMemory_1.createClusterMemoryPath)()})\n`);
312
+ }
313
+ catch (e) {
314
+ process.stdout.write(`(警告: 写入记忆失败: ${e?.message || e})\n`);
315
+ }
316
+ };
317
+ persistMemory();
318
+ try {
319
+ const result = await ctx.api.emr.createCluster(params);
320
+ process.stdout.write(`\n✓ 集群创建成功: ClusterId=${result.ClusterId}` +
321
+ (result.OperationId ? `, OperationId=${result.OperationId}` : "") +
322
+ "\n");
323
+ return result;
324
+ }
325
+ catch (err) {
326
+ process.stdout.write(`\n✗ 集群创建失败: ${err?.message || err}\n` +
327
+ `(已保存本次填写的默认值到记忆,下次可直接复用)\n`);
328
+ throw err;
329
+ }
330
+ }
331
+ finally {
332
+ prompt.close();
333
+ }
334
+ }
335
+ };
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deleteCluster = void 0;
4
+ const zod_1 = require("zod");
5
+ const ecsApi_1 = require("../../services/ecsApi");
6
+ function formatClusters(clusters, max = 10) {
7
+ const formatted = clusters.map((cluster) => {
8
+ const name = cluster.ClusterName ? `, ${cluster.ClusterName}` : "";
9
+ return `${cluster.ClusterId}${name} (${cluster.ClusterState || "UNKNOWN"})`;
10
+ });
11
+ if (formatted.length <= max)
12
+ return formatted.join(", ");
13
+ return `${formatted.slice(0, max).join(", ")} ... (+${formatted.length - max} more)`;
14
+ }
15
+ function collectClustersFromPreviousList(lastResult) {
16
+ const items = Array.isArray(lastResult?.Items)
17
+ ? lastResult.Items
18
+ : [];
19
+ const deletableClusters = [];
20
+ const skippedTerminatedClusters = [];
21
+ for (const item of items) {
22
+ const clusterId = item?.ClusterId;
23
+ if (typeof clusterId !== "string" || clusterId.length === 0)
24
+ continue;
25
+ const clusterName = typeof item?.ClusterName === "string" && item.ClusterName.length > 0
26
+ ? item.ClusterName
27
+ : undefined;
28
+ const clusterState = String(item?.ClusterState || "").toUpperCase();
29
+ const cluster = {
30
+ ClusterId: clusterId,
31
+ ClusterName: clusterName,
32
+ ClusterState: clusterState || undefined
33
+ };
34
+ if (clusterState === ecsApi_1.EmrClusterState.TERMINATED) {
35
+ skippedTerminatedClusters.push(cluster);
36
+ continue;
37
+ }
38
+ deletableClusters.push(cluster);
39
+ }
40
+ return { deletableClusters, skippedTerminatedClusters };
41
+ }
42
+ function buildSkippedArchivedNotices(clusters) {
43
+ if (clusters.length === 0)
44
+ return [];
45
+ return [
46
+ {
47
+ type: "message",
48
+ level: "warn",
49
+ text: "以下集群处于 TERMINATED(已归档)状态,无需再释放,已自动跳过。"
50
+ },
51
+ {
52
+ type: "kv",
53
+ key: "clusters",
54
+ value: formatClusters(clusters)
55
+ }
56
+ ];
57
+ }
58
+ function buildDeleteClusterConfirmMessage(input) {
59
+ const clusterName = typeof input.ClusterName === "string" && input.ClusterName.length > 0
60
+ ? ` ClusterName=${input.ClusterName}`
61
+ : "";
62
+ const clusterState = typeof input.ClusterState === "string" && input.ClusterState.length > 0
63
+ ? ` ClusterState=${input.ClusterState}`
64
+ : "";
65
+ return `⚠ 执行高风险 Tool: deleteCluster${clusterName}${clusterState} ${JSON.stringify(input)}`;
66
+ }
67
+ exports.deleteCluster = {
68
+ name: "deleteCluster",
69
+ description: "Release (delete) one EMR cluster via Volcengine OpenAPI (ReleaseCluster). " +
70
+ "For cleanup flows, the agent may first call listClusters and then expand deleteCluster {FromPreviousList:true} into multiple one-by-one deletions. " +
71
+ "Do not generate delete tasks for clusters in TERMINATED state because archived clusters in that state do not support ReleaseCluster.",
72
+ riskLevel: "high",
73
+ input: zod_1.z
74
+ .object({
75
+ ClusterId: zod_1.z.string().min(1).optional(),
76
+ ClusterName: zod_1.z.string().min(1).optional(),
77
+ ClusterState: zod_1.z.string().min(1).optional(),
78
+ FromPreviousList: zod_1.z.boolean().optional()
79
+ })
80
+ .refine((input) => !!input.ClusterId || !!input.FromPreviousList, {
81
+ message: "One of ClusterId or FromPreviousList=true is required"
82
+ }),
83
+ prepareStep(step, ctx) {
84
+ if (!step.input?.FromPreviousList)
85
+ return;
86
+ if (ctx.memory["__lastToolName"] !== "listClusters") {
87
+ throw new Error("deleteCluster with FromPreviousList requires previous step to be listClusters");
88
+ }
89
+ const { deletableClusters, skippedTerminatedClusters } = collectClustersFromPreviousList(ctx.memory["__lastToolResult"]);
90
+ if (deletableClusters.length === 0) {
91
+ return {
92
+ action: "skip",
93
+ reason: skippedTerminatedClusters.length > 0
94
+ ? "no deletable clusters found from previous list"
95
+ : "no clusters found from previous list",
96
+ notices: buildSkippedArchivedNotices(skippedTerminatedClusters)
97
+ };
98
+ }
99
+ return {
100
+ action: "continue",
101
+ steps: deletableClusters.map(({ ClusterId, ClusterName, ClusterState }) => ({
102
+ tool: "deleteCluster",
103
+ input: { ClusterId, ClusterName, ClusterState }
104
+ })),
105
+ notices: [
106
+ {
107
+ type: "message",
108
+ level: "info",
109
+ text: `Expanded deleteCluster into ${deletableClusters.length} per-cluster step(s).` +
110
+ (skippedTerminatedClusters.length
111
+ ? ` Skipped ${skippedTerminatedClusters.length} TERMINATED cluster(s).`
112
+ : "")
113
+ },
114
+ ...buildSkippedArchivedNotices(skippedTerminatedClusters)
115
+ ]
116
+ };
117
+ },
118
+ formatRiskConfirmMessage(input) {
119
+ return buildDeleteClusterConfirmMessage(input);
120
+ },
121
+ async execute(input, ctx) {
122
+ if (!input.ClusterId) {
123
+ throw new Error("deleteCluster requires a concrete ClusterId. For FromPreviousList flows, the agent must expand it into per-cluster deleteCluster steps first.");
124
+ }
125
+ return ctx.api.emr.releaseCluster({ ClusterId: input.ClusterId });
126
+ }
127
+ };
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findClustersToCleanup = void 0;
4
+ const zod_1 = require("zod");
5
+ const ecsApi_1 = require("../../services/ecsApi");
6
+ const DAY = 86400000;
7
+ const DEFAULT_STATES = [
8
+ ecsApi_1.EmrClusterState.TERMINATED,
9
+ ecsApi_1.EmrClusterState.TERMINATED_WITH_ERROR,
10
+ ecsApi_1.EmrClusterState.FAILED,
11
+ ecsApi_1.EmrClusterState.SHUTDOWN
12
+ ];
13
+ exports.findClustersToCleanup = {
14
+ name: "findClustersToCleanup",
15
+ description: "[Deprecated] Find cleanup candidates by wrapping listClusters. " +
16
+ "Prefer planning `listClusters` first and then `deleteCluster` with {FromPreviousList:true}. " +
17
+ "`states` MUST use the official uppercase enum: " +
18
+ ecsApi_1.CLUSTER_STATES.join(", "),
19
+ input: zod_1.z.object({
20
+ olderThanDays: zod_1.z.number().int().positive().optional(),
21
+ states: zod_1.z.array(zod_1.z.nativeEnum(ecsApi_1.EmrClusterState)).optional()
22
+ }),
23
+ async execute(input, ctx) {
24
+ const olderThanDays = input?.olderThanDays ?? 7;
25
+ const threshold = Date.now() - olderThanDays * DAY;
26
+ const result = await ctx.api.emr.listClusters({
27
+ ClusterStates: input?.states && input.states.length ? input.states : DEFAULT_STATES,
28
+ CreateTimeBefore: threshold
29
+ });
30
+ return result.Items;
31
+ }
32
+ };
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.emrApi = exports.emrTools = void 0;
4
+ const listClusters_1 = require("./listClusters");
5
+ const deleteCluster_1 = require("./deleteCluster");
6
+ const createCluster_1 = require("./createCluster");
7
+ const ecsApi_1 = require("../../services/ecsApi");
8
+ Object.defineProperty(exports, "emrApi", { enumerable: true, get: function () { return ecsApi_1.emrApi; } });
9
+ exports.emrTools = {
10
+ listClusters: listClusters_1.listClusters,
11
+ deleteCluster: deleteCluster_1.deleteCluster,
12
+ createCluster: createCluster_1.createCluster
13
+ };
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listClusters = void 0;
4
+ const zod_1 = require("zod");
5
+ const ecsApi_1 = require("../../services/ecsApi");
6
+ const STATE_ALIASES = {
7
+ "已关停": ecsApi_1.EmrClusterState.SHUTDOWN,
8
+ "关停": ecsApi_1.EmrClusterState.SHUTDOWN,
9
+ "运行中": ecsApi_1.EmrClusterState.RUNNING,
10
+ "已终止": ecsApi_1.EmrClusterState.TERMINATED,
11
+ "已创建": ecsApi_1.EmrClusterState.RUNNING,
12
+ "创建中": ecsApi_1.EmrClusterState.CREATING,
13
+ "失败": ecsApi_1.EmrClusterState.FAILED,
14
+ "异常": ecsApi_1.EmrClusterState.EXCEPTION,
15
+ "已停止": ecsApi_1.EmrClusterState.PAUSED,
16
+ "停止中": ecsApi_1.EmrClusterState.PAUSING
17
+ };
18
+ function normalizeOneState(raw) {
19
+ if (!raw)
20
+ return undefined;
21
+ const upper = raw.toUpperCase();
22
+ if (ecsApi_1.CLUSTER_STATES.includes(upper)) {
23
+ return upper;
24
+ }
25
+ return STATE_ALIASES[raw];
26
+ }
27
+ function normalizeStates(states) {
28
+ if (!states || !states.length)
29
+ return undefined;
30
+ const out = [];
31
+ for (const s of states) {
32
+ const v = normalizeOneState(String(s));
33
+ if (v)
34
+ out.push(v);
35
+ else
36
+ throw new Error(`Invalid ClusterState \`${s}\`. Allowed: ${ecsApi_1.CLUSTER_STATES.join(", ")}`);
37
+ }
38
+ return out;
39
+ }
40
+ const TagSchema = zod_1.z.object({ Key: zod_1.z.string(), Value: zod_1.z.string() });
41
+ const ClusterStateEnum = zod_1.z.nativeEnum(ecsApi_1.EmrClusterState);
42
+ exports.listClusters = {
43
+ name: "listClusters",
44
+ description: "List EMR clusters via Volcengine OpenAPI (ListClusters). ClusterStates MUST use the official uppercase enum: " +
45
+ ecsApi_1.CLUSTER_STATES.join(", "),
46
+ input: zod_1.z.object({
47
+ ClusterName: zod_1.z.string().optional(),
48
+ ClusterId: zod_1.z.string().optional(),
49
+ ReleaseVersion: zod_1.z.string().optional(),
50
+ ProjectName: zod_1.z.string().optional(),
51
+ CreateTimeBefore: zod_1.z.union([zod_1.z.number(), zod_1.z.string()]).optional(),
52
+ CreateTimeAfter: zod_1.z.union([zod_1.z.number(), zod_1.z.string()]).optional(),
53
+ ClusterIds: zod_1.z.array(zod_1.z.string()).optional(),
54
+ ClusterTypes: zod_1.z.array(zod_1.z.string()).optional(),
55
+ ClusterStates: zod_1.z.array(ClusterStateEnum).optional(),
56
+ ChargeTypes: zod_1.z.array(zod_1.z.enum(["PRE", "POST"])).optional(),
57
+ Tags: zod_1.z.array(TagSchema).optional(),
58
+ MaxResults: zod_1.z.number().int().positive().max(100).optional(),
59
+ NextToken: zod_1.z.string().optional()
60
+ }),
61
+ async execute(input, ctx) {
62
+ const safeInput = { ...(input || {}) };
63
+ if (safeInput.ClusterStates) {
64
+ safeInput.ClusterStates = normalizeStates(safeInput.ClusterStates);
65
+ }
66
+ return ctx.api.emr.listClusters(safeInput);
67
+ }
68
+ };
@@ -4,12 +4,21 @@ exports.deleteCluster = void 0;
4
4
  const zod_1 = require("zod");
5
5
  exports.deleteCluster = {
6
6
  name: "deleteCluster",
7
- description: "Release (delete) an EMR cluster via Volcengine OpenAPI (ReleaseCluster)",
7
+ description: "Release (delete) one EMR cluster via Volcengine OpenAPI (ReleaseCluster). " +
8
+ "For cleanup flows, the agent may first call listClusters and then expand deleteCluster {FromPreviousList:true} into multiple one-by-one deletions.",
8
9
  riskLevel: "high",
9
- input: zod_1.z.object({
10
- ClusterId: zod_1.z.string().min(1)
10
+ input: zod_1.z
11
+ .object({
12
+ ClusterId: zod_1.z.string().min(1).optional(),
13
+ FromPreviousList: zod_1.z.boolean().optional()
14
+ })
15
+ .refine((input) => !!input.ClusterId || !!input.FromPreviousList, {
16
+ message: "One of ClusterId or FromPreviousList=true is required"
11
17
  }),
12
18
  async execute(input, ctx) {
19
+ if (!input.ClusterId) {
20
+ throw new Error("deleteCluster requires a concrete ClusterId. For FromPreviousList flows, the agent must expand it into per-cluster deleteCluster steps first.");
21
+ }
13
22
  return ctx.api.emr.releaseCluster({ ClusterId: input.ClusterId });
14
23
  }
15
24
  };
@@ -3,9 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findClustersToCleanup = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const emrApi_1 = require("../../services/emrApi");
6
+ const DAY = 86400000;
7
+ const DEFAULT_STATES = [
8
+ emrApi_1.EmrClusterState.TERMINATED,
9
+ emrApi_1.EmrClusterState.TERMINATED_WITH_ERROR,
10
+ emrApi_1.EmrClusterState.FAILED,
11
+ emrApi_1.EmrClusterState.SHUTDOWN
12
+ ];
6
13
  exports.findClustersToCleanup = {
7
14
  name: "findClustersToCleanup",
8
- description: "Find EMR clusters that are in shutdown-like states and older than N days (default 7). " +
15
+ description: "[Deprecated] Find cleanup candidates by wrapping listClusters. " +
16
+ "Prefer planning `listClusters` first and then `deleteCluster` with {FromPreviousList:true}. " +
9
17
  "`states` MUST use the official uppercase enum: " +
10
18
  emrApi_1.CLUSTER_STATES.join(", "),
11
19
  input: zod_1.z.object({
@@ -13,6 +21,12 @@ exports.findClustersToCleanup = {
13
21
  states: zod_1.z.array(zod_1.z.nativeEnum(emrApi_1.EmrClusterState)).optional()
14
22
  }),
15
23
  async execute(input, ctx) {
16
- return ctx.api.emr.findClustersToCleanup(input || {});
24
+ const olderThanDays = input?.olderThanDays ?? 7;
25
+ const threshold = Date.now() - olderThanDays * DAY;
26
+ const result = await ctx.api.emr.listClusters({
27
+ ClusterStates: input?.states && input.states.length ? input.states : DEFAULT_STATES,
28
+ CreateTimeBefore: threshold
29
+ });
30
+ return result.Items;
17
31
  }
18
32
  };
@@ -3,13 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.emrApi = exports.emrTools = void 0;
4
4
  const listClusters_1 = require("./listClusters");
5
5
  const deleteCluster_1 = require("./deleteCluster");
6
- const findClustersToCleanup_1 = require("./findClustersToCleanup");
7
6
  const createCluster_1 = require("./createCluster");
8
7
  const emrApi_1 = require("../../services/emrApi");
9
8
  Object.defineProperty(exports, "emrApi", { enumerable: true, get: function () { return emrApi_1.emrApi; } });
10
9
  exports.emrTools = {
11
10
  listClusters: listClusters_1.listClusters,
12
11
  deleteCluster: deleteCluster_1.deleteCluster,
13
- findClustersToCleanup: findClustersToCleanup_1.findClustersToCleanup,
14
12
  createCluster: createCluster_1.createCluster
15
13
  };