@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,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.emrApi = exports.CLUSTER_STATES = exports.EmrClusterState = void 0;
4
+ exports.listClusters = listClusters;
5
+ exports.listAllClusters = listAllClusters;
6
+ exports.releaseCluster = releaseCluster;
7
+ exports.createCluster = createCluster;
8
+ exports.findClustersToCleanup = findClustersToCleanup;
9
+ const client_1 = require("./client");
10
+ var EmrClusterState;
11
+ (function (EmrClusterState) {
12
+ EmrClusterState["PENDING_FOR_PAYMENT"] = "PENDING_FOR_PAYMENT";
13
+ EmrClusterState["CREATING"] = "CREATING";
14
+ EmrClusterState["RUNNING"] = "RUNNING";
15
+ EmrClusterState["WARNING"] = "WARNING";
16
+ EmrClusterState["EXCEPTION"] = "EXCEPTION";
17
+ EmrClusterState["RESTORING"] = "RESTORING";
18
+ EmrClusterState["PAUSING"] = "PAUSING";
19
+ EmrClusterState["PAUSED"] = "PAUSED";
20
+ EmrClusterState["TERMINATING"] = "TERMINATING";
21
+ EmrClusterState["TERMINATED"] = "TERMINATED";
22
+ EmrClusterState["TERMINATED_WITH_ERROR"] = "TERMINATED_WITH_ERROR";
23
+ EmrClusterState["FAILED"] = "FAILED";
24
+ EmrClusterState["SHUTDOWN"] = "SHUTDOWN";
25
+ })(EmrClusterState || (exports.EmrClusterState = EmrClusterState = {}));
26
+ exports.CLUSTER_STATES = Object.values(EmrClusterState);
27
+ function compactBody(input) {
28
+ const out = {};
29
+ for (const [k, v] of Object.entries(input)) {
30
+ if (v === undefined || v === null || v === "")
31
+ continue;
32
+ if (Array.isArray(v) && v.length === 0)
33
+ continue;
34
+ out[k] = v;
35
+ }
36
+ return out;
37
+ }
38
+ function toMs(v) {
39
+ if (v === undefined || v === null || v === "")
40
+ return undefined;
41
+ const n = typeof v === "number" ? v : Number(v);
42
+ if (!Number.isFinite(n))
43
+ return undefined;
44
+ // 10 位 = 秒级,13 位 = 毫秒级,统一转毫秒
45
+ return n < 1e12 ? Math.trunc(n) * 1000 : Math.trunc(n);
46
+ }
47
+ function toMsString(v) {
48
+ const ms = toMs(v);
49
+ return ms === undefined ? undefined : String(ms);
50
+ }
51
+ async function listClusters(params = {}) {
52
+ const body = compactBody({
53
+ ClusterName: params.ClusterName,
54
+ ClusterId: params.ClusterId,
55
+ ReleaseVersion: params.ReleaseVersion,
56
+ ProjectName: params.ProjectName,
57
+ CreateTimeBefore: toMsString(params.CreateTimeBefore),
58
+ CreateTimeAfter: toMsString(params.CreateTimeAfter),
59
+ ClusterIds: params.ClusterIds,
60
+ ClusterTypes: params.ClusterTypes,
61
+ ClusterStates: params.ClusterStates,
62
+ ChargeTypes: params.ChargeTypes,
63
+ Tags: params.Tags,
64
+ MaxResults: params.MaxResults,
65
+ NextToken: params.NextToken
66
+ });
67
+ const result = await (0, client_1.callEmr)("ListClusters", body);
68
+ return {
69
+ Items: Array.isArray(result?.Items) ? result.Items : [],
70
+ TotalCount: typeof result?.TotalCount === "number" ? result.TotalCount : 0,
71
+ MaxResults: typeof result?.MaxResults === "number" ? result.MaxResults : 0,
72
+ NextToken: result?.NextToken || undefined
73
+ };
74
+ }
75
+ async function listAllClusters(params = {}) {
76
+ const { hardLimit, NextToken: initialToken, MaxResults, ...rest } = params;
77
+ const pageSize = Math.min(Math.max(MaxResults ?? 50, 1), 100);
78
+ const all = [];
79
+ let token = initialToken;
80
+ let safety = 0;
81
+ do {
82
+ const page = await listClusters({
83
+ ...rest,
84
+ MaxResults: pageSize,
85
+ NextToken: token
86
+ });
87
+ all.push(...page.Items);
88
+ if (hardLimit && all.length >= hardLimit) {
89
+ return all.slice(0, hardLimit);
90
+ }
91
+ token = page.NextToken;
92
+ safety++;
93
+ if (safety > 1000) {
94
+ throw new Error("[listAllClusters] safety break: more than 1000 pages, check NextToken loop");
95
+ }
96
+ } while (token);
97
+ return all;
98
+ }
99
+ function matchArchived(msg) {
100
+ return /marked\s+archived|could not operate before fixed/i.test(msg);
101
+ }
102
+ function matchNotFound(msg) {
103
+ return /not\s*found|does\s*not\s*exist|不存在/i.test(msg);
104
+ }
105
+ function matchAlreadyReleased(msg) {
106
+ return /already\s+released|已释放|已删除/i.test(msg);
107
+ }
108
+ async function releaseCluster(params) {
109
+ try {
110
+ const resp = await (0, client_1.callEmr)("ReleaseCluster", params);
111
+ return {
112
+ ClusterId: params.ClusterId,
113
+ OperationId: resp?.OperationId,
114
+ OperateId: resp?.OperateId,
115
+ Skipped: false
116
+ };
117
+ }
118
+ catch (e) {
119
+ const msg = String(e?.message || e);
120
+ if (matchArchived(msg)) {
121
+ return {
122
+ ClusterId: params.ClusterId,
123
+ Skipped: true,
124
+ Reason: "ARCHIVED",
125
+ Message: "集群已被归档(archived),无法再操作;需要联系火山侧解除归档后再释放。"
126
+ };
127
+ }
128
+ if (matchNotFound(msg)) {
129
+ return {
130
+ ClusterId: params.ClusterId,
131
+ Skipped: true,
132
+ Reason: "NOT_FOUND",
133
+ Message: "集群不存在(可能已被清理)。"
134
+ };
135
+ }
136
+ if (matchAlreadyReleased(msg)) {
137
+ return {
138
+ ClusterId: params.ClusterId,
139
+ Skipped: true,
140
+ Reason: "ALREADY_RELEASED",
141
+ Message: "集群已释放,无需重复操作。"
142
+ };
143
+ }
144
+ throw e;
145
+ }
146
+ }
147
+ async function createCluster(params) {
148
+ const body = compactBody({ ...params });
149
+ const resp = await (0, client_1.callEmr)("CreateCluster", body);
150
+ return {
151
+ ClusterId: resp?.ClusterId || "",
152
+ OperationId: resp?.OperationId
153
+ };
154
+ }
155
+ const DAY = 86400000;
156
+ const SHUTDOWN_STATES = [
157
+ EmrClusterState.TERMINATED,
158
+ EmrClusterState.TERMINATED_WITH_ERROR,
159
+ EmrClusterState.FAILED,
160
+ EmrClusterState.SHUTDOWN
161
+ ];
162
+ async function findClustersToCleanup({ olderThanDays = 7, states } = {}) {
163
+ const threshold = Date.now() - olderThanDays * DAY;
164
+ const targetStates = states && states.length ? states : SHUTDOWN_STATES;
165
+ const items = await listAllClusters({
166
+ ClusterStates: targetStates,
167
+ CreateTimeBefore: threshold
168
+ });
169
+ return items;
170
+ }
171
+ exports.emrApi = {
172
+ listClusters,
173
+ listAllClusters,
174
+ releaseCluster,
175
+ findClustersToCleanup,
176
+ createCluster
177
+ };
@@ -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("../../../shared/confirm");
6
+ const createClusterMemory_1 = require("../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,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deleteCluster = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.deleteCluster = {
6
+ name: "deleteCluster",
7
+ description: "Release (delete) an EMR cluster via Volcengine OpenAPI (ReleaseCluster)",
8
+ riskLevel: "high",
9
+ input: zod_1.z.object({
10
+ ClusterId: zod_1.z.string().min(1)
11
+ }),
12
+ async execute(input, ctx) {
13
+ return ctx.api.emr.releaseCluster({ ClusterId: input.ClusterId });
14
+ }
15
+ };
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findClustersToCleanup = void 0;
4
+ const zod_1 = require("zod");
5
+ const emr_1 = require("../emr");
6
+ exports.findClustersToCleanup = {
7
+ name: "findClustersToCleanup",
8
+ description: "Find EMR clusters that are in shutdown-like states and older than N days (default 7). " +
9
+ "`states` MUST use the official uppercase enum: " +
10
+ emr_1.CLUSTER_STATES.join(", "),
11
+ input: zod_1.z.object({
12
+ olderThanDays: zod_1.z.number().int().positive().optional(),
13
+ states: zod_1.z.array(zod_1.z.nativeEnum(emr_1.EmrClusterState)).optional()
14
+ }),
15
+ async execute(input, ctx) {
16
+ return ctx.api.emr.findClustersToCleanup(input || {});
17
+ }
18
+ };
@@ -0,0 +1,15 @@
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 findClustersToCleanup_1 = require("./findClustersToCleanup");
7
+ const createCluster_1 = require("./createCluster");
8
+ const emr_1 = require("../emr");
9
+ Object.defineProperty(exports, "emrApi", { enumerable: true, get: function () { return emr_1.emrApi; } });
10
+ exports.emrTools = {
11
+ listClusters: listClusters_1.listClusters,
12
+ deleteCluster: deleteCluster_1.deleteCluster,
13
+ findClustersToCleanup: findClustersToCleanup_1.findClustersToCleanup,
14
+ createCluster: createCluster_1.createCluster
15
+ };
@@ -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 emr_1 = require("../emr");
6
+ const STATE_ALIASES = {
7
+ "已关停": emr_1.EmrClusterState.SHUTDOWN,
8
+ "关停": emr_1.EmrClusterState.SHUTDOWN,
9
+ "运行中": emr_1.EmrClusterState.RUNNING,
10
+ "已终止": emr_1.EmrClusterState.TERMINATED,
11
+ "已创建": emr_1.EmrClusterState.RUNNING,
12
+ "创建中": emr_1.EmrClusterState.CREATING,
13
+ "失败": emr_1.EmrClusterState.FAILED,
14
+ "异常": emr_1.EmrClusterState.EXCEPTION,
15
+ "已停止": emr_1.EmrClusterState.PAUSED,
16
+ "停止中": emr_1.EmrClusterState.PAUSING
17
+ };
18
+ function normalizeOneState(raw) {
19
+ if (!raw)
20
+ return undefined;
21
+ const upper = raw.toUpperCase();
22
+ if (emr_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: ${emr_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(emr_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
+ emr_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
+ };