@volc-emr/emr-cli 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +336 -0
- package/dist/agent/agent.js +19 -0
- package/dist/agent/executor.js +25 -0
- package/dist/agent/llmPlanner.js +131 -0
- package/dist/agent/planner.js +12 -0
- package/dist/agent/types.js +2 -0
- package/dist/index.js +182 -0
- package/dist/runtime/config.js +73 -0
- package/dist/runtime/confirm.js +92 -0
- package/dist/runtime/createClusterMemory.js +56 -0
- package/dist/runtime/llm.js +64 -0
- package/dist/runtime/logger.js +8 -0
- package/dist/runtime/memory.js +4 -0
- package/dist/services/emrApi.js +181 -0
- package/dist/services/volcApi.js +53 -0
- package/dist/tools/base.js +2 -0
- package/dist/tools/emr/createCluster.js +335 -0
- package/dist/tools/emr/deleteCluster.js +15 -0
- package/dist/tools/emr/findClustersToCleanup.js +18 -0
- package/dist/tools/emr/index.js +15 -0
- package/dist/tools/emr/listClusters.js +68 -0
- package/dist/tools/registry.js +11 -0
- package/dist/utils/prompt.js +9 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const agent_1 = require("./agent/agent");
|
|
6
|
+
const registry_1 = require("./tools/registry");
|
|
7
|
+
const memory_1 = require("./runtime/memory");
|
|
8
|
+
const config_1 = require("./runtime/config");
|
|
9
|
+
const confirm_1 = require("./runtime/confirm");
|
|
10
|
+
const program = new commander_1.Command();
|
|
11
|
+
program
|
|
12
|
+
.name("volc-emr-agent")
|
|
13
|
+
.description("Volcengine EMR CLI Agent (real OpenAPI)")
|
|
14
|
+
.version("0.1.0");
|
|
15
|
+
program
|
|
16
|
+
.command("run <task>")
|
|
17
|
+
.description("Plan and execute a natural language EMR task")
|
|
18
|
+
.option("--dry-run", "Only show plan")
|
|
19
|
+
.option("-y, --yes", "Auto approve high-risk tools (skip confirm)")
|
|
20
|
+
.option("--region <region>", "Override region, default cn-beijing")
|
|
21
|
+
.action(async (task, opts) => {
|
|
22
|
+
if (opts.accessKey)
|
|
23
|
+
process.env.VOLC_ACCESSKEY = opts.accessKey;
|
|
24
|
+
if (opts.secretKey)
|
|
25
|
+
process.env.VOLC_SECRETKEY = opts.secretKey;
|
|
26
|
+
if (opts.region)
|
|
27
|
+
process.env.VOLC_REGION = opts.region;
|
|
28
|
+
(0, config_1.resolveCredentials)();
|
|
29
|
+
const agent = new agent_1.Agent({
|
|
30
|
+
api: registry_1.apiRegistry,
|
|
31
|
+
memory: memory_1.memory
|
|
32
|
+
});
|
|
33
|
+
await agent.run(task, {
|
|
34
|
+
dryRun: opts.dryRun,
|
|
35
|
+
autoApprove: opts.yes
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
program
|
|
39
|
+
.command("init")
|
|
40
|
+
.description("Interactive wizard to initialize local credentials")
|
|
41
|
+
.option("--access-key <ak>", "Non-interactive access key")
|
|
42
|
+
.option("--secret-key <sk>", "Non-interactive secret key")
|
|
43
|
+
.option("--region <region>", "Default region", "cn-beijing")
|
|
44
|
+
.option("--llm-endpoint <url>", "Optional LLM endpoint (OpenAI compatible)")
|
|
45
|
+
.option("--llm-api-key <key>", "Optional LLM API key")
|
|
46
|
+
.option("--llm-model <model>", "Optional LLM model name")
|
|
47
|
+
.option("-f, --force", "Overwrite existing config without asking")
|
|
48
|
+
.action(async (opts) => {
|
|
49
|
+
const existing = (0, config_1.readLocalConfig)();
|
|
50
|
+
const hasExisting = !!(existing.accessKey || existing.secretKey);
|
|
51
|
+
if (hasExisting && !opts.force) {
|
|
52
|
+
console.log(`Detected existing config: ${(0, config_1.configFilePath)()}`);
|
|
53
|
+
const ok = await (0, confirm_1.confirm)("Overwrite existing credentials?");
|
|
54
|
+
if (!ok) {
|
|
55
|
+
console.log("Cancelled. No changes made.");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const needInteractive = !opts.accessKey || !opts.secretKey;
|
|
60
|
+
const session = needInteractive ? (0, confirm_1.createPromptSession)() : null;
|
|
61
|
+
try {
|
|
62
|
+
const accessKey = opts.accessKey ||
|
|
63
|
+
(await session.ask("VOLC Access Key", {
|
|
64
|
+
defaultValue: existing.accessKey
|
|
65
|
+
}));
|
|
66
|
+
if (!accessKey) {
|
|
67
|
+
console.error("Access key is required.");
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const secretKey = opts.secretKey ||
|
|
72
|
+
(await session.ask("VOLC Secret Key", { silent: true }));
|
|
73
|
+
if (!secretKey) {
|
|
74
|
+
console.error("Secret key is required.");
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const region = opts.region ||
|
|
79
|
+
(session
|
|
80
|
+
? await session.ask("Region", {
|
|
81
|
+
defaultValue: existing.region || "cn-beijing"
|
|
82
|
+
})
|
|
83
|
+
: existing.region || "cn-beijing") ||
|
|
84
|
+
"cn-beijing";
|
|
85
|
+
const llmEndpoint = opts.llmEndpoint ??
|
|
86
|
+
(session
|
|
87
|
+
? await session.ask("LLM endpoint (empty to skip)", {
|
|
88
|
+
defaultValue: existing.llm?.endpoint || ""
|
|
89
|
+
})
|
|
90
|
+
: existing.llm?.endpoint || "");
|
|
91
|
+
let llmApiKey = opts.llmApiKey;
|
|
92
|
+
let llmModel = opts.llmModel;
|
|
93
|
+
if (llmEndpoint && session) {
|
|
94
|
+
llmApiKey =
|
|
95
|
+
llmApiKey ??
|
|
96
|
+
(await session.ask("LLM API key", { silent: true }));
|
|
97
|
+
llmModel =
|
|
98
|
+
llmModel ??
|
|
99
|
+
(await session.ask("LLM model", {
|
|
100
|
+
defaultValue: existing.llm?.model || ""
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
const payload = { accessKey, secretKey, region };
|
|
104
|
+
if (llmEndpoint) {
|
|
105
|
+
payload.llm = {
|
|
106
|
+
endpoint: llmEndpoint,
|
|
107
|
+
apiKey: llmApiKey || existing.llm?.apiKey,
|
|
108
|
+
model: llmModel || existing.llm?.model
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const file = (0, config_1.writeLocalConfig)(payload);
|
|
112
|
+
console.log(`\n✓ Saved to ${file}`);
|
|
113
|
+
console.log({
|
|
114
|
+
accessKey: `${accessKey.slice(0, 4)}****`,
|
|
115
|
+
secretKey: "********",
|
|
116
|
+
region,
|
|
117
|
+
llm: llmEndpoint
|
|
118
|
+
? {
|
|
119
|
+
endpoint: llmEndpoint,
|
|
120
|
+
apiKey: payload.llm.apiKey ? "********" : undefined,
|
|
121
|
+
model: payload.llm.model || undefined
|
|
122
|
+
}
|
|
123
|
+
: "(not configured)"
|
|
124
|
+
});
|
|
125
|
+
console.log("\nTip: run `volc-emr-agent run \"列出集群\"` to verify.");
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
session?.close();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const config = program.command("config").description("Manage local credentials");
|
|
132
|
+
config
|
|
133
|
+
.command("set-credentials")
|
|
134
|
+
.description("Save AK/SK/region to local config (~/.volc-emr/config.json)")
|
|
135
|
+
.requiredOption("--access-key <ak>")
|
|
136
|
+
.requiredOption("--secret-key <sk>")
|
|
137
|
+
.option("--region <region>", "default cn-beijing", "cn-beijing")
|
|
138
|
+
.action((opts) => {
|
|
139
|
+
const file = (0, config_1.writeLocalConfig)({
|
|
140
|
+
accessKey: opts.accessKey,
|
|
141
|
+
secretKey: opts.secretKey,
|
|
142
|
+
region: opts.region
|
|
143
|
+
});
|
|
144
|
+
console.log(`Saved: ${file}`);
|
|
145
|
+
});
|
|
146
|
+
config
|
|
147
|
+
.command("set-llm")
|
|
148
|
+
.description("Save LLM endpoint config to local config")
|
|
149
|
+
.requiredOption("--endpoint <url>", "OpenAI-compatible Chat Completions URL")
|
|
150
|
+
.option("--api-key <key>")
|
|
151
|
+
.option("--model <model>")
|
|
152
|
+
.action((opts) => {
|
|
153
|
+
const file = (0, config_1.writeLocalConfig)({
|
|
154
|
+
llm: {
|
|
155
|
+
endpoint: opts.endpoint,
|
|
156
|
+
apiKey: opts.apiKey,
|
|
157
|
+
model: opts.model
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
console.log(`Saved LLM config: ${file}`);
|
|
161
|
+
});
|
|
162
|
+
config
|
|
163
|
+
.command("show")
|
|
164
|
+
.description("Show current local config (secrets are masked)")
|
|
165
|
+
.action(() => {
|
|
166
|
+
const cfg = (0, config_1.readLocalConfig)();
|
|
167
|
+
const masked = {
|
|
168
|
+
...cfg,
|
|
169
|
+
accessKey: cfg.accessKey ? `${cfg.accessKey.slice(0, 4)}****` : undefined,
|
|
170
|
+
secretKey: cfg.secretKey ? "********" : undefined,
|
|
171
|
+
llm: cfg.llm
|
|
172
|
+
? {
|
|
173
|
+
endpoint: cfg.llm.endpoint,
|
|
174
|
+
apiKey: cfg.llm.apiKey ? "********" : undefined,
|
|
175
|
+
model: cfg.llm.model
|
|
176
|
+
}
|
|
177
|
+
: undefined
|
|
178
|
+
};
|
|
179
|
+
console.log("file:", (0, config_1.configFilePath)());
|
|
180
|
+
console.log(masked);
|
|
181
|
+
});
|
|
182
|
+
program.parse();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readLocalConfig = readLocalConfig;
|
|
7
|
+
exports.writeLocalConfig = writeLocalConfig;
|
|
8
|
+
exports.clearLlmConfig = clearLlmConfig;
|
|
9
|
+
exports.resolveCredentials = resolveCredentials;
|
|
10
|
+
exports.resolveLlmConfig = resolveLlmConfig;
|
|
11
|
+
exports.configFilePath = configFilePath;
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const CONFIG_DIR = process.env.VOLC_EMR_CONFIG_DIR || path_1.default.join(os_1.default.homedir(), ".volc-emr");
|
|
16
|
+
const CONFIG_FILE = path_1.default.join(CONFIG_DIR, "config.json");
|
|
17
|
+
function readLocalConfig() {
|
|
18
|
+
try {
|
|
19
|
+
if (!fs_1.default.existsSync(CONFIG_FILE))
|
|
20
|
+
return {};
|
|
21
|
+
return JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function writeLocalConfig(cfg) {
|
|
28
|
+
fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
29
|
+
const prev = readLocalConfig();
|
|
30
|
+
let mergedLlm;
|
|
31
|
+
if (cfg.llm === null) {
|
|
32
|
+
mergedLlm = undefined;
|
|
33
|
+
}
|
|
34
|
+
else if (cfg.llm || prev.llm) {
|
|
35
|
+
mergedLlm = { ...(prev.llm || {}), ...(cfg.llm || {}) };
|
|
36
|
+
}
|
|
37
|
+
const merged = { ...prev, ...cfg };
|
|
38
|
+
if (mergedLlm && Object.keys(mergedLlm).length > 0) {
|
|
39
|
+
merged.llm = mergedLlm;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
delete merged.llm;
|
|
43
|
+
}
|
|
44
|
+
fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), {
|
|
45
|
+
mode: 0o600
|
|
46
|
+
});
|
|
47
|
+
return CONFIG_FILE;
|
|
48
|
+
}
|
|
49
|
+
function clearLlmConfig() {
|
|
50
|
+
return writeLocalConfig({ llm: null });
|
|
51
|
+
}
|
|
52
|
+
function resolveCredentials(cli = {}) {
|
|
53
|
+
const local = readLocalConfig();
|
|
54
|
+
const accessKey = cli.accessKey || process.env.VOLC_ACCESSKEY || local.accessKey;
|
|
55
|
+
const secretKey = cli.secretKey || process.env.VOLC_SECRETKEY || local.secretKey;
|
|
56
|
+
const region = cli.region || process.env.VOLC_REGION || local.region || "cn-beijing";
|
|
57
|
+
if (!accessKey || !secretKey) {
|
|
58
|
+
throw new Error("未找到 Volcengine 凭证。请通过 CLI 参数、环境变量(VOLC_ACCESSKEY/VOLC_SECRETKEY)或本地配置(~/.volc-emr/config.json)提供。");
|
|
59
|
+
}
|
|
60
|
+
return { accessKey, secretKey, region };
|
|
61
|
+
}
|
|
62
|
+
function resolveLlmConfig(cli = {}) {
|
|
63
|
+
const local = readLocalConfig().llm || {};
|
|
64
|
+
const endpoint = cli.endpoint || process.env.VOLC_LLM_ENDPOINT || local.endpoint;
|
|
65
|
+
const apiKey = cli.apiKey || process.env.VOLC_LLM_API_KEY || local.apiKey;
|
|
66
|
+
const model = cli.model || process.env.VOLC_LLM_MODEL || local.model;
|
|
67
|
+
if (!endpoint)
|
|
68
|
+
return null;
|
|
69
|
+
return { endpoint, apiKey, model };
|
|
70
|
+
}
|
|
71
|
+
function configFilePath() {
|
|
72
|
+
return CONFIG_FILE;
|
|
73
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.confirm = confirm;
|
|
7
|
+
exports.createPromptSession = createPromptSession;
|
|
8
|
+
const readline_1 = __importDefault(require("readline"));
|
|
9
|
+
function createLineSource() {
|
|
10
|
+
const stdin = process.stdin;
|
|
11
|
+
stdin.setEncoding("utf8");
|
|
12
|
+
let buffer = "";
|
|
13
|
+
const queue = [];
|
|
14
|
+
const waiters = [];
|
|
15
|
+
let ended = false;
|
|
16
|
+
const flush = () => {
|
|
17
|
+
let idx;
|
|
18
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
19
|
+
const line = buffer.slice(0, idx).replace(/\r$/, "");
|
|
20
|
+
buffer = buffer.slice(idx + 1);
|
|
21
|
+
const w = waiters.shift();
|
|
22
|
+
if (w)
|
|
23
|
+
w(line);
|
|
24
|
+
else
|
|
25
|
+
queue.push(line);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
stdin.on("data", (chunk) => {
|
|
29
|
+
buffer += chunk;
|
|
30
|
+
flush();
|
|
31
|
+
});
|
|
32
|
+
stdin.on("end", () => {
|
|
33
|
+
ended = true;
|
|
34
|
+
if (buffer.length) {
|
|
35
|
+
const w = waiters.shift();
|
|
36
|
+
if (w)
|
|
37
|
+
w(buffer);
|
|
38
|
+
else
|
|
39
|
+
queue.push(buffer);
|
|
40
|
+
buffer = "";
|
|
41
|
+
}
|
|
42
|
+
while (waiters.length) {
|
|
43
|
+
const w = waiters.shift();
|
|
44
|
+
w && w("");
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
nextLine() {
|
|
49
|
+
if (queue.length)
|
|
50
|
+
return Promise.resolve(queue.shift());
|
|
51
|
+
if (ended)
|
|
52
|
+
return Promise.resolve("");
|
|
53
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
54
|
+
},
|
|
55
|
+
stop() {
|
|
56
|
+
stdin.pause();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function confirm(question) {
|
|
61
|
+
const rl = readline_1.default.createInterface({
|
|
62
|
+
input: process.stdin,
|
|
63
|
+
output: process.stdout
|
|
64
|
+
});
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
rl.question(`${question} (y/n): `, (answer) => {
|
|
67
|
+
rl.close();
|
|
68
|
+
resolve(answer.toLowerCase() === "y");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function createPromptSession() {
|
|
73
|
+
const source = createLineSource();
|
|
74
|
+
process.stdin.resume();
|
|
75
|
+
return {
|
|
76
|
+
async ask(question, options = {}) {
|
|
77
|
+
const { defaultValue, silent } = options;
|
|
78
|
+
const label = defaultValue
|
|
79
|
+
? `${question} [${defaultValue}]: `
|
|
80
|
+
: `${question}: `;
|
|
81
|
+
process.stdout.write(label);
|
|
82
|
+
const line = await source.nextLine();
|
|
83
|
+
const value = (line || "").trim();
|
|
84
|
+
if (silent)
|
|
85
|
+
process.stdout.write("\n");
|
|
86
|
+
return value || defaultValue || "";
|
|
87
|
+
},
|
|
88
|
+
close() {
|
|
89
|
+
source.stop();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readCreateClusterMemory = readCreateClusterMemory;
|
|
7
|
+
exports.writeCreateClusterMemory = writeCreateClusterMemory;
|
|
8
|
+
exports.clearCreateClusterMemory = clearCreateClusterMemory;
|
|
9
|
+
exports.createClusterMemoryPath = createClusterMemoryPath;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const MEMORY_DIR = process.env.VOLC_EMR_CONFIG_DIR || path_1.default.join(os_1.default.homedir(), ".volc-emr");
|
|
14
|
+
const MEMORY_FILE = path_1.default.join(MEMORY_DIR, "create-cluster-memory.json");
|
|
15
|
+
function readCreateClusterMemory() {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs_1.default.existsSync(MEMORY_FILE))
|
|
18
|
+
return {};
|
|
19
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(MEMORY_FILE, "utf-8"));
|
|
20
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function writeCreateClusterMemory(patch) {
|
|
27
|
+
fs_1.default.mkdirSync(MEMORY_DIR, { recursive: true, mode: 0o700 });
|
|
28
|
+
const prev = readCreateClusterMemory();
|
|
29
|
+
const merged = { ...prev };
|
|
30
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
31
|
+
if (v === undefined || v === null)
|
|
32
|
+
continue;
|
|
33
|
+
if (typeof v === "string" && !v.trim())
|
|
34
|
+
continue;
|
|
35
|
+
if (Array.isArray(v) && v.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
merged[k] = v;
|
|
38
|
+
}
|
|
39
|
+
merged.updatedAt = Date.now();
|
|
40
|
+
fs_1.default.writeFileSync(MEMORY_FILE, JSON.stringify(merged, null, 2), {
|
|
41
|
+
mode: 0o600
|
|
42
|
+
});
|
|
43
|
+
return merged;
|
|
44
|
+
}
|
|
45
|
+
function clearCreateClusterMemory() {
|
|
46
|
+
try {
|
|
47
|
+
if (fs_1.default.existsSync(MEMORY_FILE))
|
|
48
|
+
fs_1.default.unlinkSync(MEMORY_FILE);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* ignore */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function createClusterMemoryPath() {
|
|
55
|
+
return MEMORY_FILE;
|
|
56
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.chatCompletion = chatCompletion;
|
|
7
|
+
const https_1 = __importDefault(require("https"));
|
|
8
|
+
const http_1 = __importDefault(require("http"));
|
|
9
|
+
const url_1 = require("url");
|
|
10
|
+
async function chatCompletion(llm, options) {
|
|
11
|
+
if (!llm.endpoint)
|
|
12
|
+
throw new Error("LLM endpoint is not configured");
|
|
13
|
+
const body = {
|
|
14
|
+
model: llm.model || "default",
|
|
15
|
+
messages: options.messages,
|
|
16
|
+
temperature: options.temperature ?? 0
|
|
17
|
+
};
|
|
18
|
+
if (options.jsonMode) {
|
|
19
|
+
body.response_format = { type: "json_object" };
|
|
20
|
+
}
|
|
21
|
+
const payload = JSON.stringify(body);
|
|
22
|
+
const url = new url_1.URL(llm.endpoint);
|
|
23
|
+
const headers = {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"Content-Length": Buffer.byteLength(payload).toString()
|
|
26
|
+
};
|
|
27
|
+
if (llm.apiKey)
|
|
28
|
+
headers["Authorization"] = `Bearer ${llm.apiKey}`;
|
|
29
|
+
const client = url.protocol === "https:" ? https_1.default : http_1.default;
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const req = client.request({
|
|
32
|
+
method: "POST",
|
|
33
|
+
hostname: url.hostname,
|
|
34
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
35
|
+
path: `${url.pathname}${url.search}`,
|
|
36
|
+
headers
|
|
37
|
+
}, (res) => {
|
|
38
|
+
const chunks = [];
|
|
39
|
+
res.on("data", (c) => chunks.push(c));
|
|
40
|
+
res.on("end", () => {
|
|
41
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
42
|
+
if ((res.statusCode || 500) >= 400) {
|
|
43
|
+
reject(new Error(`LLM HTTP ${res.statusCode}: ${raw}`));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const json = JSON.parse(raw);
|
|
48
|
+
const content = json?.choices?.[0]?.message?.content;
|
|
49
|
+
if (typeof content !== "string") {
|
|
50
|
+
reject(new Error(`Unexpected LLM response shape: ${raw.slice(0, 200)}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
resolve(content);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
reject(new Error(`Parse LLM response failed: ${e.message}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
req.on("error", reject);
|
|
61
|
+
req.write(payload);
|
|
62
|
+
req.end();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
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 volcApi_1 = require("./volcApi");
|
|
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
|
+
console.log(body);
|
|
68
|
+
const result = await (0, volcApi_1.callEmr)("ListClusters", body);
|
|
69
|
+
return {
|
|
70
|
+
Items: Array.isArray(result?.Items) ? result.Items : [],
|
|
71
|
+
TotalCount: typeof result?.TotalCount === "number" ? result.TotalCount : 0,
|
|
72
|
+
MaxResults: typeof result?.MaxResults === "number" ? result.MaxResults : 0,
|
|
73
|
+
NextToken: result?.NextToken || undefined
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function listAllClusters(params = {}) {
|
|
77
|
+
const { hardLimit, NextToken: initialToken, MaxResults, ...rest } = params;
|
|
78
|
+
const pageSize = Math.min(Math.max(MaxResults ?? 50, 1), 100);
|
|
79
|
+
const all = [];
|
|
80
|
+
let token = initialToken;
|
|
81
|
+
let safety = 0;
|
|
82
|
+
do {
|
|
83
|
+
const page = await listClusters({
|
|
84
|
+
...rest,
|
|
85
|
+
MaxResults: pageSize,
|
|
86
|
+
NextToken: token
|
|
87
|
+
});
|
|
88
|
+
all.push(...page.Items);
|
|
89
|
+
if (hardLimit && all.length >= hardLimit) {
|
|
90
|
+
return all.slice(0, hardLimit);
|
|
91
|
+
}
|
|
92
|
+
token = page.NextToken;
|
|
93
|
+
safety++;
|
|
94
|
+
if (safety > 1000) {
|
|
95
|
+
throw new Error("[listAllClusters] safety break: more than 1000 pages, check NextToken loop");
|
|
96
|
+
}
|
|
97
|
+
} while (token);
|
|
98
|
+
return all;
|
|
99
|
+
}
|
|
100
|
+
function matchArchived(msg) {
|
|
101
|
+
return /marked\s+archived|could not operate before fixed/i.test(msg);
|
|
102
|
+
}
|
|
103
|
+
function matchNotFound(msg) {
|
|
104
|
+
return /not\s*found|does\s*not\s*exist|不存在/i.test(msg);
|
|
105
|
+
}
|
|
106
|
+
function matchAlreadyReleased(msg) {
|
|
107
|
+
return /already\s+released|已释放|已删除/i.test(msg);
|
|
108
|
+
}
|
|
109
|
+
async function releaseCluster(params) {
|
|
110
|
+
try {
|
|
111
|
+
const resp = await (0, volcApi_1.callEmr)("ReleaseCluster", params);
|
|
112
|
+
return {
|
|
113
|
+
ClusterId: params.ClusterId,
|
|
114
|
+
OperationId: resp?.OperationId,
|
|
115
|
+
OperateId: resp?.OperateId,
|
|
116
|
+
Skipped: false
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
const msg = String(e?.message || e);
|
|
121
|
+
if (matchArchived(msg)) {
|
|
122
|
+
return {
|
|
123
|
+
ClusterId: params.ClusterId,
|
|
124
|
+
Skipped: true,
|
|
125
|
+
Reason: "ARCHIVED",
|
|
126
|
+
Message: "集群已被归档(archived),无法再操作;需要联系火山侧解除归档后再释放。"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (matchNotFound(msg)) {
|
|
130
|
+
return {
|
|
131
|
+
ClusterId: params.ClusterId,
|
|
132
|
+
Skipped: true,
|
|
133
|
+
Reason: "NOT_FOUND",
|
|
134
|
+
Message: "集群不存在(可能已被清理)。"
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (matchAlreadyReleased(msg)) {
|
|
138
|
+
return {
|
|
139
|
+
ClusterId: params.ClusterId,
|
|
140
|
+
Skipped: true,
|
|
141
|
+
Reason: "ALREADY_RELEASED",
|
|
142
|
+
Message: "集群已释放,无需重复操作。"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function createCluster(params) {
|
|
149
|
+
const body = compactBody({ ...params });
|
|
150
|
+
console.log("createCluster params:", params);
|
|
151
|
+
console.log(body);
|
|
152
|
+
const resp = await (0, volcApi_1.callEmr)("CreateCluster", body);
|
|
153
|
+
return {
|
|
154
|
+
ClusterId: resp?.ClusterId || "",
|
|
155
|
+
OperationId: resp?.OperationId
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const DAY = 86400000;
|
|
159
|
+
const SHUTDOWN_STATES = [
|
|
160
|
+
EmrClusterState.TERMINATED,
|
|
161
|
+
EmrClusterState.TERMINATED_WITH_ERROR,
|
|
162
|
+
EmrClusterState.FAILED,
|
|
163
|
+
EmrClusterState.SHUTDOWN
|
|
164
|
+
];
|
|
165
|
+
async function findClustersToCleanup({ olderThanDays = 7, states } = {}) {
|
|
166
|
+
const threshold = Date.now() - olderThanDays * DAY;
|
|
167
|
+
const targetStates = states && states.length ? states : SHUTDOWN_STATES;
|
|
168
|
+
const items = await listAllClusters({
|
|
169
|
+
ClusterStates: targetStates,
|
|
170
|
+
CreateTimeBefore: threshold
|
|
171
|
+
});
|
|
172
|
+
console.log(items);
|
|
173
|
+
return items;
|
|
174
|
+
}
|
|
175
|
+
exports.emrApi = {
|
|
176
|
+
listClusters,
|
|
177
|
+
listAllClusters,
|
|
178
|
+
releaseCluster,
|
|
179
|
+
findClustersToCleanup,
|
|
180
|
+
createCluster
|
|
181
|
+
};
|