@volc-emr/emr-cli 0.1.0-beta.1 → 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.
- package/README.md +81 -269
- package/dist/agent/executor.js +39 -3
- package/dist/agent/llmPlanner.js +4 -17
- package/dist/commands/config.js +57 -0
- package/dist/commands/init.js +102 -0
- package/dist/commands/run.js +41 -0
- package/dist/core/agent.js +30 -0
- package/dist/core/executor.js +36 -0
- package/dist/core/llm-planner.js +131 -0
- package/dist/core/planner.js +12 -0
- package/dist/core/registry.js +11 -0
- package/dist/core/tool.js +2 -0
- package/dist/core/types.js +2 -0
- package/dist/index.js +8 -183
- package/dist/integrations/volc/client.js +53 -0
- package/dist/integrations/volc/createClusterMemory.js +56 -0
- package/dist/integrations/volc/emr.js +177 -0
- package/dist/integrations/volc/tools/createCluster.js +335 -0
- package/dist/integrations/volc/tools/deleteCluster.js +15 -0
- package/dist/integrations/volc/tools/findClustersToCleanup.js +18 -0
- package/dist/integrations/volc/tools/index.js +15 -0
- package/dist/integrations/volc/tools/listClusters.js +68 -0
- package/dist/services/ecsApi.js +159 -0
- package/dist/services/emrApi.js +0 -18
- package/dist/shared/config.js +73 -0
- package/dist/shared/confirm.js +92 -0
- package/dist/shared/llm.js +64 -0
- package/dist/shared/logger.js +122 -0
- package/dist/shared/memory.js +4 -0
- package/dist/shared/prompt.js +9 -0
- package/dist/tools/ecs/createCluster.js +335 -0
- package/dist/tools/ecs/deleteCluster.js +127 -0
- package/dist/tools/ecs/findClustersToCleanup.js +32 -0
- package/dist/tools/ecs/index.js +13 -0
- package/dist/tools/ecs/listClusters.js +68 -0
- package/dist/tools/emr/deleteCluster.js +12 -3
- package/dist/tools/emr/findClustersToCleanup.js +16 -2
- package/dist/tools/emr/index.js +0 -2
- package/dist/tools/registry.js +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,335 +1,147 @@
|
|
|
1
|
-
#
|
|
1
|
+
# emr-cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
>
|
|
5
|
-
> Tool-first · Plan → Execute · LLM Planner(可选)· 真实 OpenAPI 调用 · 本地凭证 / 偏好记忆。
|
|
3
|
+
一个面向 Volcengine EMR 的 CLI Agent。
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
它把自然语言任务拆成 `Plan -> Execute`,再调用真实 OpenAPI 执行,适合本地开发和命令行运维场景。
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
2. 高风险步骤前弹确认(或 `-y` 跳过)
|
|
11
|
-
3. 交由 Executor 依次调用 Tool
|
|
12
|
-
4. Tool 用 Zod 校验 LLM 输入 → 调用真实 EMR OpenAPI → 把结果写回 stdout
|
|
13
|
-
5. 创建集群这类复合 Tool,会**再次**交互式向你追问缺失字段并回填常用默认值
|
|
7
|
+
## 功能
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
- 自然语言执行 EMR 任务
|
|
10
|
+
- 支持 `dry-run` 查看计划
|
|
11
|
+
- 高风险操作执行前确认
|
|
12
|
+
- 本地保存 Volcengine 凭证和 LLM 配置
|
|
13
|
+
- 当前内置 `listClusters`、`deleteCluster`、`createCluster`
|
|
16
14
|
|
|
17
|
-
##
|
|
15
|
+
## 环境要求
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
| `listClusters` | [ListClusters](https://www.volcengine.com/docs/6491/1208305) | low | 支持全字段过滤 / 分页 / 中文状态别名归一化 |
|
|
22
|
-
| `findClustersToCleanup` | ListClusters | low | 组合查询:超过 N 天 + 处于 SHUTDOWN 类状态 |
|
|
23
|
-
| `deleteCluster` | [ReleaseCluster](https://www.volcengine.com/docs/6491/1208304) | **high** | 自动识别归档 / 已释放 / 不存在 → 幂等跳过而非中断 |
|
|
24
|
-
| `createCluster` | [CreateCluster](https://www.volcengine.com/docs/6491/1208307) | **high** | 交互式补全 13 类必填字段、PRE/POST 计费分支、节点组向导 |
|
|
25
|
-
|
|
26
|
-
---
|
|
17
|
+
- Node.js `>= 18`
|
|
18
|
+
- 一个兼容 OpenAI Chat Completions 的 LLM 接口
|
|
27
19
|
|
|
28
20
|
## 安装
|
|
29
21
|
|
|
22
|
+
全局安装:
|
|
23
|
+
|
|
30
24
|
```bash
|
|
31
|
-
# 通过 npm 全局安装(推荐)
|
|
32
25
|
npm install -g @volc-emr/emr-cli@beta
|
|
33
|
-
|
|
34
|
-
# 验证(CLI 命令名仍是 volc-emr-agent)
|
|
35
|
-
volc-emr-agent --help
|
|
26
|
+
emr-cli --help
|
|
36
27
|
```
|
|
37
28
|
|
|
38
|
-
|
|
29
|
+
本地开发:
|
|
39
30
|
|
|
40
31
|
```bash
|
|
41
|
-
git clone <repo>
|
|
42
|
-
cd console-cli
|
|
43
32
|
npm install
|
|
44
33
|
npm run build
|
|
45
|
-
npm link
|
|
34
|
+
npm link
|
|
35
|
+
emr-cli --help
|
|
46
36
|
```
|
|
47
37
|
|
|
48
|
-
|
|
38
|
+
## 快速开始
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
## 快速上手(三步)
|
|
40
|
+
初始化配置:
|
|
53
41
|
|
|
54
42
|
```bash
|
|
55
|
-
|
|
56
|
-
volc-emr-agent init
|
|
57
|
-
|
|
58
|
-
# 2) 跑第一条命令(只打印 Plan,不执行)
|
|
59
|
-
volc-emr-agent run "列出所有运行中的集群" --dry-run
|
|
60
|
-
|
|
61
|
-
# 3) 真正执行
|
|
62
|
-
volc-emr-agent run "列出所有运行中的集群"
|
|
43
|
+
emr-cli init
|
|
63
44
|
```
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
## 凭证与配置
|
|
68
|
-
|
|
69
|
-
优先级统一为:**CLI 参数 > 环境变量 > 本地配置文件**。
|
|
70
|
-
|
|
71
|
-
### 本地配置文件位置
|
|
72
|
-
|
|
73
|
-
| 文件 | 内容 | 权限 |
|
|
74
|
-
|---|---|---|
|
|
75
|
-
| `~/.volc-emr/config.json` | Volcengine AK/SK/Region + LLM endpoint/apiKey/model | `0600` |
|
|
76
|
-
| `~/.volc-emr/create-cluster-memory.json` | 创建集群向导的常用偏好(VPC/子网/可用区/机型等) | `0600` |
|
|
77
|
-
|
|
78
|
-
可通过 `VOLC_EMR_CONFIG_DIR` 环境变量整体改位置(测试 / 多环境友好)。
|
|
79
|
-
|
|
80
|
-
### 写配置
|
|
46
|
+
配置 LLM:
|
|
81
47
|
|
|
82
48
|
```bash
|
|
83
|
-
|
|
84
|
-
volc-emr-agent init
|
|
85
|
-
|
|
86
|
-
# 已有配置?加 --force 跳过确认
|
|
87
|
-
volc-emr-agent init --force
|
|
88
|
-
|
|
89
|
-
# 非交互(CI 友好)
|
|
90
|
-
volc-emr-agent init \
|
|
91
|
-
--access-key "$VOLC_ACCESSKEY" \
|
|
92
|
-
--secret-key "$VOLC_SECRETKEY" \
|
|
93
|
-
--region cn-beijing
|
|
94
|
-
|
|
95
|
-
# 单独写凭证
|
|
96
|
-
volc-emr-agent config set-credentials \
|
|
97
|
-
--access-key xxx --secret-key yyy --region cn-beijing
|
|
98
|
-
|
|
99
|
-
# 单独写 LLM
|
|
100
|
-
volc-emr-agent config set-llm \
|
|
49
|
+
emr-cli config set-llm \
|
|
101
50
|
--endpoint https://ark.cn-beijing.volces.com/api/v3/chat/completions \
|
|
102
|
-
--api-key
|
|
103
|
-
--model
|
|
51
|
+
--api-key <your-api-key> \
|
|
52
|
+
--model <your-model>
|
|
104
53
|
```
|
|
105
54
|
|
|
106
|
-
|
|
55
|
+
查看执行计划:
|
|
107
56
|
|
|
108
57
|
```bash
|
|
109
|
-
|
|
110
|
-
export VOLC_SECRETKEY=yyy
|
|
111
|
-
export VOLC_REGION=cn-beijing
|
|
112
|
-
|
|
113
|
-
# LLM(可选;未配置时 Planner 会直接报错,不做 rule-based 兜底)
|
|
114
|
-
export VOLC_LLM_ENDPOINT=https://ark.cn-beijing.volces.com/api/v3/chat/completions
|
|
115
|
-
export VOLC_LLM_API_KEY=ark-xxx
|
|
116
|
-
export VOLC_LLM_MODEL=doubao-seed-character-251128
|
|
58
|
+
emr-cli run "列出所有运行中的集群" --dry-run
|
|
117
59
|
```
|
|
118
60
|
|
|
119
|
-
|
|
61
|
+
直接执行:
|
|
120
62
|
|
|
121
63
|
```bash
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# 改用自定义目录
|
|
126
|
-
VOLC_EMR_CONFIG_DIR=/tmp/volc-test volc-emr-agent init
|
|
64
|
+
emr-cli run "列出所有运行中的集群"
|
|
65
|
+
emr-cli run "清理一周前已关停的集群" -y
|
|
66
|
+
emr-cli run "帮我创建一个 3.7.0 的 Hadoop 集群"
|
|
127
67
|
```
|
|
128
68
|
|
|
129
|
-
|
|
69
|
+
## 命令
|
|
130
70
|
|
|
131
|
-
|
|
71
|
+
| 命令 | 说明 |
|
|
72
|
+
|----------------------------------|-------------------------------|
|
|
73
|
+
| `emr-cli init` | 交互式写入 AK/SK、Region 和可选 LLM 配置 |
|
|
74
|
+
| `emr-cli run <task>` | 执行一条自然语言任务 |
|
|
75
|
+
| `emr-cli config set-credentials` | 单独写入凭证 |
|
|
76
|
+
| `emr-cli config set-llm` | 单独写入 LLM 配置 |
|
|
77
|
+
| `emr-cli config show` | 查看当前本地配置 |
|
|
132
78
|
|
|
133
|
-
|
|
79
|
+
常用选项:
|
|
134
80
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
```
|
|
81
|
+
- `--dry-run`:只生成计划,不执行
|
|
82
|
+
- `-y, --yes`:自动确认高风险操作
|
|
83
|
+
- `--region <region>`:临时覆盖区域
|
|
139
84
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
# 看 Plan、不执行
|
|
144
|
-
volc-emr-agent run "清理一周前已关停的集群" --dry-run
|
|
85
|
+
## 配置
|
|
145
86
|
|
|
146
|
-
|
|
147
|
-
volc-emr-agent run "清理一周前已关停的集群" -y
|
|
87
|
+
优先级:
|
|
148
88
|
|
|
149
|
-
|
|
150
|
-
volc-emr-agent run "清理 14 天前已关停的集群"
|
|
151
|
-
```
|
|
89
|
+
`CLI 参数 > 环境变量 > 本地配置文件`
|
|
152
90
|
|
|
153
|
-
|
|
91
|
+
默认配置文件:
|
|
154
92
|
|
|
155
|
-
|
|
93
|
+
- `~/.volc-emr/config.json`
|
|
94
|
+
- `~/.volc-emr/create-cluster-memory.json`
|
|
156
95
|
|
|
157
|
-
|
|
158
|
-
volc-emr-agent run "帮我创建一个 3.7.0 的 Hadoop 集群"
|
|
159
|
-
```
|
|
96
|
+
可通过 `VOLC_EMR_CONFIG_DIR` 修改配置目录。
|
|
160
97
|
|
|
161
|
-
|
|
98
|
+
环境变量示例:
|
|
162
99
|
|
|
100
|
+
```bash
|
|
101
|
+
export VOLC_ACCESSKEY=xxx
|
|
102
|
+
export VOLC_SECRETKEY=yyy
|
|
103
|
+
export VOLC_REGION=cn-beijing
|
|
104
|
+
export VOLC_LLM_ENDPOINT=https://ark.cn-beijing.volces.com/api/v3/chat/completions
|
|
105
|
+
export VOLC_LLM_API_KEY=ark-xxx
|
|
106
|
+
export VOLC_LLM_MODEL=your-model
|
|
163
107
|
```
|
|
164
|
-
=== 创建 EMR 集群:补全配置 ===
|
|
165
|
-
(已加载上次记忆,路径: /Users/you/.volc-emr/create-cluster-memory.json)
|
|
166
|
-
|
|
167
|
-
集群类型 (可选: Hadoop / Presto / ...) [Hadoop]: ⏎
|
|
168
|
-
集群版本 (ReleaseVersion) [3.7.0]: ⏎
|
|
169
|
-
集群名称 [OpenApiHadoop3.7.0-a7x2m]: ⏎
|
|
170
|
-
付费类型 (可选: POST / PRE) [POST]: ⏎
|
|
171
|
-
VpcId (如 vpc-xxx) [vpc-abc123]: ⏎ ← 来自上次记忆
|
|
172
|
-
SecurityGroupId (如 sg-xxx) [sg-def456]: ⏎ ← 来自上次记忆
|
|
173
|
-
可用区 ZoneId [cn-beijing-b]: ⏎
|
|
174
|
-
|
|
175
|
-
-- 节点组配置 (共 2 组) --
|
|
176
|
-
[节点组 1]
|
|
177
|
-
节点组类型 (可选: MASTER / CORE / TASK / GATEWAY) [MASTER]: ⏎
|
|
178
|
-
节点数 [3]: ⏎
|
|
179
|
-
SubnetIds (逗号分隔) [subnet-xyz]: ⏎
|
|
180
|
-
EcsInstanceTypes [ecs.g3i.2xlarge]: ⏎
|
|
181
|
-
...
|
|
182
|
-
|
|
183
|
-
=== 即将创建集群,配置如下 ===
|
|
184
|
-
{ ...完整 JSON... }
|
|
185
|
-
确认使用以上配置创建集群? (y/n): y
|
|
186
|
-
|
|
187
|
-
(已更新常用默认值记忆: /Users/you/.volc-emr/create-cluster-memory.json)
|
|
188
|
-
✓ 集群创建成功: ClusterId=emr-xxx, OperationId=op-xxx
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**特性**:
|
|
192
|
-
|
|
193
|
-
- **记忆**:用户确认后立刻写 `create-cluster-memory.json`。即使 OpenAPI 随后返回错误,下次依然能复用你填过的网络/节点偏好,不用再填一遍
|
|
194
|
-
- **中文状态 / 大小写自动归一化**:LLM 写成 "已关停" / "Running" 都能被翻译成官方枚举
|
|
195
|
-
- **MASTER + CORE 自动补齐**:Hadoop 类集群强制至少两个节点组
|
|
196
|
-
- **ChargeType=PRE 自动展开包年包月子向导**(周期 / 自动续费)
|
|
197
|
-
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
## 命令总览
|
|
201
|
-
|
|
202
|
-
| 命令 | 作用 |
|
|
203
|
-
|---|---|
|
|
204
|
-
| `volc-emr-agent init [--force]` | 交互式写入 AK/SK、可选一起配 LLM |
|
|
205
|
-
| `volc-emr-agent config set-credentials --access-key ... --secret-key ...` | 单独更新凭证 |
|
|
206
|
-
| `volc-emr-agent config set-llm --endpoint ... [--api-key ...] [--model ...]` | 单独更新 LLM |
|
|
207
|
-
| `volc-emr-agent config show` | 查看本地配置(脱敏) |
|
|
208
|
-
| `volc-emr-agent run <task> [--dry-run] [-y] [--region ...]` | 执行一条自然语言任务 |
|
|
209
|
-
|
|
210
|
-
运行选项:
|
|
211
|
-
|
|
212
|
-
- `--dry-run`:只打印 Plan,不调用任何 OpenAPI
|
|
213
|
-
- `-y, --yes`:对 `riskLevel: "high"` 的 Tool 自动确认
|
|
214
|
-
- `--region <region>`:临时覆盖当前会话的区域(不写配置)
|
|
215
|
-
|
|
216
|
-
---
|
|
217
108
|
|
|
218
|
-
##
|
|
109
|
+
## 当前结构
|
|
219
110
|
|
|
220
|
-
```
|
|
111
|
+
```text
|
|
221
112
|
src/
|
|
222
|
-
├── index.ts
|
|
113
|
+
├── index.ts
|
|
223
114
|
├── agent/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
│ ├── llmPlanner.ts # 把 Zod 输入翻成 JSON Schema 喂给 LLM
|
|
227
|
-
│ ├── executor.ts # 依次执行 Plan,触发高风险确认
|
|
228
|
-
│ └── types.ts
|
|
115
|
+
├── commands/
|
|
116
|
+
├── runtime/
|
|
229
117
|
├── services/
|
|
230
|
-
│ ├── volcApi.ts # @volcengine/openapi 签名 + 错误解析
|
|
231
|
-
│ └── emrApi.ts # listClusters / releaseCluster / createCluster ...
|
|
232
118
|
├── tools/
|
|
233
|
-
│
|
|
234
|
-
|
|
235
|
-
│ └── emr/
|
|
236
|
-
│ ├── index.ts
|
|
237
|
-
│ ├── listClusters.ts
|
|
238
|
-
│ ├── findClustersToCleanup.ts
|
|
239
|
-
│ ├── deleteCluster.ts # → ReleaseCluster
|
|
240
|
-
│ └── createCluster.ts # 交互式向导 + 记忆
|
|
241
|
-
├── runtime/
|
|
242
|
-
│ ├── config.ts # AK/SK + LLM 持久化
|
|
243
|
-
│ ├── createClusterMemory.ts # 创建集群专用的偏好记忆(独立文件)
|
|
244
|
-
│ ├── confirm.ts # TTY / pipe 双模安全的 prompt
|
|
245
|
-
│ ├── llm.ts # OpenAI-compatible Chat Completions 客户端
|
|
246
|
-
│ ├── memory.ts # 进程内简易 KV
|
|
247
|
-
│ └── logger.ts
|
|
248
|
-
└── utils/prompt.ts
|
|
119
|
+
│ └── ecs/
|
|
120
|
+
└── utils/
|
|
249
121
|
```
|
|
250
122
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
## 关键设计
|
|
254
|
-
|
|
255
|
-
### 1. Tool-first
|
|
123
|
+
目录职责:
|
|
256
124
|
|
|
257
|
-
|
|
125
|
+
- `src/index.ts`:CLI 入口
|
|
126
|
+
- `src/commands`:命令注册,当前包含 `run`、`init`、`config`
|
|
127
|
+
- `src/agent`:规划与执行流程
|
|
128
|
+
- `src/tools/ecs`:EMR 相关工具实现
|
|
129
|
+
- `src/services`:OpenAPI 调用封装
|
|
130
|
+
- `src/runtime`:配置、日志、交互和本地记忆
|
|
258
131
|
|
|
259
|
-
|
|
132
|
+
## 开发
|
|
260
133
|
|
|
134
|
+
```bash
|
|
135
|
+
npm run build
|
|
136
|
+
npm run typecheck
|
|
137
|
+
node dist/index.js --help
|
|
261
138
|
```
|
|
262
|
-
用户自然语言
|
|
263
|
-
↓
|
|
264
|
-
Planner(LLM, Zod 校验)→ PlanStep[]
|
|
265
|
-
↓
|
|
266
|
-
Executor(读 registry, 拦截 high-risk, 调用 Tool)
|
|
267
|
-
↓
|
|
268
|
-
真实 Volcengine OpenAPI
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### 3. 枚举三层防御(以 `ClusterStates` 为例)
|
|
272
|
-
|
|
273
|
-
- **Schema 层**:Tool input 用 `z.nativeEnum(EmrClusterState)`,LLM 收到带 `enum` 的 JSON Schema
|
|
274
|
-
- **Prompt 层**:Tool description 里显式印合法枚举清单
|
|
275
|
-
- **执行层**:Tool `execute()` 内部再做一次大小写 + 中文别名兜底
|
|
276
|
-
|
|
277
|
-
### 4. OpenAPI 错误可读化
|
|
278
|
-
|
|
279
|
-
[volcApi.ts](src/services/volcApi.ts) 对火山返回做结构化解析,错误消息会带 `Code / RequestId / body` 等排障必需的上下文,而不是一句干巴巴的 `Internal error`。
|
|
280
|
-
|
|
281
|
-
### 5. 记忆分文件,互不污染
|
|
282
|
-
|
|
283
|
-
- `config.json`:**身份**(AK/SK、LLM token)。敏感,少变。
|
|
284
|
-
- `create-cluster-memory.json`:**偏好**(VPC/子网/ZoneId/机型)。常变,可分享模板。
|
|
285
|
-
|
|
286
|
-
写入时机在 **确认后立刻写**(而不是 API 成功后),哪怕 OpenAPI 挂了,你下次也不用重填网络信息。
|
|
287
|
-
|
|
288
|
-
---
|
|
289
|
-
|
|
290
|
-
## LLM 配置提示
|
|
291
|
-
|
|
292
|
-
Planner 要求必须配置 LLM(OpenAI-compatible `/chat/completions`)。已验证可用的:
|
|
293
|
-
|
|
294
|
-
- **火山方舟 Ark**:
|
|
295
|
-
```
|
|
296
|
-
endpoint: https://ark.cn-beijing.volces.com/api/v3/chat/completions
|
|
297
|
-
model: doubao-seed-character-251128(或其他 doubao 模型)
|
|
298
|
-
```
|
|
299
|
-
- **OpenAI 官方**:
|
|
300
|
-
```
|
|
301
|
-
endpoint: https://api.openai.com/v1/chat/completions
|
|
302
|
-
model: gpt-4o-mini
|
|
303
|
-
```
|
|
304
|
-
- **任何兼容 OpenAI 协议的网关**(Together / Groq / Fireworks / 本地 vLLM 等)
|
|
305
|
-
|
|
306
|
-
如果模型不支持 `response_format=json_object`,Planner 会退化到 "字符串 JSON + markdown fence 清洗" 的 robust 模式,无需额外改配置。
|
|
307
|
-
|
|
308
|
-
---
|
|
309
|
-
|
|
310
|
-
## 故障排查
|
|
311
|
-
|
|
312
|
-
| 现象 | 原因 / 处理 |
|
|
313
|
-
|---|---|
|
|
314
|
-
| `[ListClusters] The request is missing Version parameter` | `@volcengine/openapi` 需要 `Version` 而非 `version` — 现已修好 |
|
|
315
|
-
| `[ListClusters] Invalid request {0}` | LLM 传了非法字段或类型,Tool 层错误消息会附上 body 预览 |
|
|
316
|
-
| `[CreateCluster] Internal error` | 5xx;抓取 `RequestId` 后去[火山工单](https://console.volcengine.com/workorder/create/),或用相同参数在 [API Explorer](https://api.volcengine.com/api-explorer/?action=CreateCluster&serviceCode=emr&version=2023-08-15) 复现 |
|
|
317
|
-
| `LLM is not configured` | Planner 没有 rule-based 兜底,请先 `volc-emr-agent config set-llm` |
|
|
318
|
-
| 清理时个别集群报 `marked archived` | 会被 `deleteCluster` 内部捕获,返回 `{ Skipped: true, Reason: "ARCHIVED" }` 并作为该步 `Done` 结果打印,不影响其它集群清理 |
|
|
319
|
-
|
|
320
|
-
想看更详细的请求/响应?可以临时编辑 [volcApi.ts](src/services/volcApi.ts) 在 `fetchApi(body)` 前后加 `console.error` 打印 body 和 response。
|
|
321
|
-
|
|
322
|
-
---
|
|
323
|
-
|
|
324
|
-
## 后续规划
|
|
325
139
|
|
|
326
|
-
|
|
327
|
-
- [ ] 更多 EMR Action:`GetCluster` / `ScaleOutNodeGroup` / `UpdateClusterAttribute`
|
|
328
|
-
- [ ] 多 profile 记忆(`--profile prod/test`)
|
|
329
|
-
- [ ] Tool catalog 热插拔、跨业务目录(非 EMR)
|
|
330
|
-
- [ ] Executor 级别的 retry / rollback / 流式输出
|
|
140
|
+
## 说明
|
|
331
141
|
|
|
332
|
-
|
|
142
|
+
- `run` 依赖 LLM 生成执行计划;未配置 LLM 时不会回退到规则模式
|
|
143
|
+
- `createCluster` 会进入交互式补全流程,并复用本地记忆
|
|
144
|
+
- `deleteCluster` 会对部分已归档或已释放场景做跳过处理
|
|
333
145
|
|
|
334
146
|
## License
|
|
335
147
|
|
package/dist/agent/executor.js
CHANGED
|
@@ -4,18 +4,52 @@ exports.executePlan = executePlan;
|
|
|
4
4
|
const registry_1 = require("../tools/registry");
|
|
5
5
|
const confirm_1 = require("../runtime/confirm");
|
|
6
6
|
const logger_1 = require("../runtime/logger");
|
|
7
|
+
function renderToolNotices(notices) {
|
|
8
|
+
for (const notice of notices || []) {
|
|
9
|
+
if (notice.type === "message") {
|
|
10
|
+
if (notice.level === "warn")
|
|
11
|
+
logger_1.ui.warn(notice.text);
|
|
12
|
+
else if (notice.level === "success")
|
|
13
|
+
logger_1.ui.success(notice.text);
|
|
14
|
+
else if (notice.level === "dim")
|
|
15
|
+
logger_1.ui.dim(notice.text);
|
|
16
|
+
else
|
|
17
|
+
logger_1.ui.info(notice.text);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
logger_1.ui.kv(notice.key, notice.value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
async function executePlan(plan, ctx, options = {}) {
|
|
24
|
+
ctx.memory["__lastToolName"] = undefined;
|
|
25
|
+
ctx.memory["__lastToolResult"] = undefined;
|
|
8
26
|
logger_1.ui.section(`Execute (${plan.length} step${plan.length > 1 ? "s" : ""})`);
|
|
9
|
-
for (
|
|
10
|
-
|
|
27
|
+
for (let i = 0; i < plan.length; i++) {
|
|
28
|
+
let step = plan[i];
|
|
29
|
+
let tool = registry_1.toolList.find((t) => t.name === step.tool);
|
|
11
30
|
if (!tool) {
|
|
12
31
|
(0, logger_1.logStep)(i, "fail", step.tool, { error: "Tool not found" });
|
|
13
32
|
throw new Error(`Tool not found: ${step.tool}`);
|
|
14
33
|
}
|
|
34
|
+
if (tool.prepareStep) {
|
|
35
|
+
const prepared = await tool.prepareStep(step, ctx);
|
|
36
|
+
if (prepared) {
|
|
37
|
+
renderToolNotices(prepared.notices);
|
|
38
|
+
if (prepared.action === "skip") {
|
|
39
|
+
(0, logger_1.logStep)(i, "skip", tool.name, { reason: prepared.reason });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (prepared.steps && prepared.steps.length > 0) {
|
|
43
|
+
plan.splice(i, 1, ...prepared.steps);
|
|
44
|
+
i--;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
15
49
|
if (tool.riskLevel === "high") {
|
|
16
50
|
const ok = options.autoApprove
|
|
17
51
|
? true
|
|
18
|
-
: await (0, confirm_1.confirm)(`⚠ 执行高风险 Tool: ${tool.name} ${JSON.stringify(step.input)}`);
|
|
52
|
+
: await (0, confirm_1.confirm)(tool.formatRiskConfirmMessage?.(step.input || {}) || `⚠ 执行高风险 Tool: ${tool.name} ${JSON.stringify(step.input)}`);
|
|
19
53
|
if (!ok) {
|
|
20
54
|
(0, logger_1.logStep)(i, "skip", tool.name, { reason: "user cancelled" });
|
|
21
55
|
continue;
|
|
@@ -24,6 +58,8 @@ async function executePlan(plan, ctx, options = {}) {
|
|
|
24
58
|
(0, logger_1.logStep)(i, "run", tool.name, { input: step.input });
|
|
25
59
|
try {
|
|
26
60
|
const result = await tool.execute(step.input, ctx);
|
|
61
|
+
ctx.memory["__lastToolName"] = tool.name;
|
|
62
|
+
ctx.memory["__lastToolResult"] = result;
|
|
27
63
|
(0, logger_1.logStep)(i, "done", tool.name, { output: result });
|
|
28
64
|
}
|
|
29
65
|
catch (err) {
|
package/dist/agent/llmPlanner.js
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.llmPlanTask = llmPlanTask;
|
|
4
4
|
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
5
5
|
const registry_1 = require("../tools/registry");
|
|
6
|
-
const emr_1 = require("../tools/emr");
|
|
7
6
|
const llm_1 = require("../runtime/llm");
|
|
8
7
|
function buildToolCatalog() {
|
|
9
8
|
return registry_1.toolList.map((t) => ({
|
|
@@ -31,7 +30,9 @@ function buildSystemPrompt() {
|
|
|
31
30
|
"- Only use tools listed above.",
|
|
32
31
|
"- `input` MUST conform to each tool's inputSchema.",
|
|
33
32
|
"- For destructive actions (riskLevel=high), only plan them if the user task clearly asks for it.",
|
|
34
|
-
"- If the task is ambiguous, return at most one safe, read-only step (e.g. listClusters)."
|
|
33
|
+
"- If the task is ambiguous, return at most one safe, read-only step (e.g. listClusters).",
|
|
34
|
+
"- For batch cleanup, first plan listClusters with filters, then plan deleteCluster with {\"FromPreviousList\":true}.",
|
|
35
|
+
"- Never generate deleteCluster tasks for clusters in `TERMINATED` state; that state means the cluster is archived and ReleaseCluster is not supported."
|
|
35
36
|
].join("\n");
|
|
36
37
|
}
|
|
37
38
|
function buildUserPrompt(task, context) {
|
|
@@ -113,19 +114,5 @@ async function llmPlanTask(task, llm) {
|
|
|
113
114
|
];
|
|
114
115
|
const content = await (0, llm_1.chatCompletion)(llm, { messages });
|
|
115
116
|
const steps = tryParsePlan(content);
|
|
116
|
-
|
|
117
|
-
const expanded = [];
|
|
118
|
-
for (const step of validated) {
|
|
119
|
-
expanded.push(step);
|
|
120
|
-
if (step.tool === "findClustersToCleanup") {
|
|
121
|
-
const found = await emr_1.emrApi.findClustersToCleanup(step.input || {});
|
|
122
|
-
for (const c of found) {
|
|
123
|
-
expanded.push({
|
|
124
|
-
tool: "deleteCluster",
|
|
125
|
-
input: { ClusterId: c.ClusterId }
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return expanded;
|
|
117
|
+
return validatePlanAgainstTools(steps);
|
|
131
118
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerConfigCommand = registerConfigCommand;
|
|
4
|
+
const config_1 = require("../runtime/config");
|
|
5
|
+
const logger_1 = require("../runtime/logger");
|
|
6
|
+
function registerConfigCommand(program) {
|
|
7
|
+
const config = program.command("config").description("Manage local credentials");
|
|
8
|
+
config
|
|
9
|
+
.command("set-credentials")
|
|
10
|
+
.description("Save AK/SK/region to local config (~/.volc-emr/config.json)")
|
|
11
|
+
.requiredOption("--access-key <ak>")
|
|
12
|
+
.requiredOption("--secret-key <sk>")
|
|
13
|
+
.option("--region <region>", "default cn-beijing", "cn-beijing")
|
|
14
|
+
.action((opts) => {
|
|
15
|
+
const file = (0, config_1.writeLocalConfig)({
|
|
16
|
+
accessKey: opts.accessKey,
|
|
17
|
+
secretKey: opts.secretKey,
|
|
18
|
+
region: opts.region
|
|
19
|
+
});
|
|
20
|
+
logger_1.ui.success(`Credentials saved: ${file}`);
|
|
21
|
+
});
|
|
22
|
+
config
|
|
23
|
+
.command("set-llm")
|
|
24
|
+
.description("Save LLM endpoint config to local config")
|
|
25
|
+
.requiredOption("--endpoint <url>", "OpenAI-compatible Chat Completions URL")
|
|
26
|
+
.option("--api-key <key>")
|
|
27
|
+
.option("--model <model>")
|
|
28
|
+
.action((opts) => {
|
|
29
|
+
const file = (0, config_1.writeLocalConfig)({
|
|
30
|
+
llm: {
|
|
31
|
+
endpoint: opts.endpoint,
|
|
32
|
+
apiKey: opts.apiKey,
|
|
33
|
+
model: opts.model
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
logger_1.ui.success(`LLM config saved: ${file}`);
|
|
37
|
+
});
|
|
38
|
+
config
|
|
39
|
+
.command("show")
|
|
40
|
+
.description("Show current local config (secrets are masked)")
|
|
41
|
+
.action(() => {
|
|
42
|
+
const cfg = (0, config_1.readLocalConfig)();
|
|
43
|
+
logger_1.ui.section("Local config");
|
|
44
|
+
logger_1.ui.kv("file", (0, config_1.configFilePath)());
|
|
45
|
+
logger_1.ui.kv("accessKey", cfg.accessKey ? `${cfg.accessKey.slice(0, 4)}****` : "(missing)");
|
|
46
|
+
logger_1.ui.kv("secretKey", cfg.secretKey ? "********" : "(missing)");
|
|
47
|
+
logger_1.ui.kv("region", cfg.region || "(missing)");
|
|
48
|
+
if (cfg.llm?.endpoint) {
|
|
49
|
+
logger_1.ui.kv("llm.endpoint", cfg.llm.endpoint);
|
|
50
|
+
logger_1.ui.kv("llm.apiKey", cfg.llm.apiKey ? "********" : "(missing)");
|
|
51
|
+
logger_1.ui.kv("llm.model", cfg.llm.model || "(missing)");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
logger_1.ui.kv("llm", "(not configured)");
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerInitCommand = registerInitCommand;
|
|
4
|
+
const config_1 = require("../runtime/config");
|
|
5
|
+
const confirm_1 = require("../runtime/confirm");
|
|
6
|
+
const logger_1 = require("../runtime/logger");
|
|
7
|
+
function registerInitCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command("init")
|
|
10
|
+
.description("Interactive wizard to initialize local credentials")
|
|
11
|
+
.option("--access-key <ak>", "Non-interactive access key")
|
|
12
|
+
.option("--secret-key <sk>", "Non-interactive secret key")
|
|
13
|
+
.option("--region <region>", "Default region", "cn-beijing")
|
|
14
|
+
.option("--llm-endpoint <url>", "Optional LLM endpoint (OpenAI compatible)")
|
|
15
|
+
.option("--llm-api-key <key>", "Optional LLM API key")
|
|
16
|
+
.option("--llm-model <model>", "Optional LLM model name")
|
|
17
|
+
.option("-f, --force", "Overwrite existing config without asking")
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
const existing = (0, config_1.readLocalConfig)();
|
|
20
|
+
const hasExisting = !!(existing.accessKey || existing.secretKey);
|
|
21
|
+
if (hasExisting && !opts.force) {
|
|
22
|
+
logger_1.ui.info(`Detected existing config: ${(0, config_1.configFilePath)()}`);
|
|
23
|
+
const ok = await (0, confirm_1.confirm)("Overwrite existing credentials?");
|
|
24
|
+
if (!ok) {
|
|
25
|
+
logger_1.ui.warn("Cancelled. No changes made.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const needInteractive = !opts.accessKey || !opts.secretKey;
|
|
30
|
+
const session = needInteractive ? (0, confirm_1.createPromptSession)() : null;
|
|
31
|
+
try {
|
|
32
|
+
const accessKey = opts.accessKey ||
|
|
33
|
+
(await session.ask("VOLC Access Key", {
|
|
34
|
+
defaultValue: existing.accessKey
|
|
35
|
+
}));
|
|
36
|
+
if (!accessKey) {
|
|
37
|
+
logger_1.ui.error("Access key is required.");
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const secretKey = opts.secretKey ||
|
|
42
|
+
(await session.ask("VOLC Secret Key", { silent: true }));
|
|
43
|
+
if (!secretKey) {
|
|
44
|
+
logger_1.ui.error("Secret key is required.");
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const region = opts.region ||
|
|
49
|
+
(session
|
|
50
|
+
? await session.ask("Region", {
|
|
51
|
+
defaultValue: existing.region || "cn-beijing"
|
|
52
|
+
})
|
|
53
|
+
: existing.region || "cn-beijing") ||
|
|
54
|
+
"cn-beijing";
|
|
55
|
+
const llmEndpoint = opts.llmEndpoint ??
|
|
56
|
+
(session
|
|
57
|
+
? await session.ask("LLM endpoint (empty to skip)", {
|
|
58
|
+
defaultValue: existing.llm?.endpoint || ""
|
|
59
|
+
})
|
|
60
|
+
: existing.llm?.endpoint || "");
|
|
61
|
+
let llmApiKey = opts.llmApiKey;
|
|
62
|
+
let llmModel = opts.llmModel;
|
|
63
|
+
if (llmEndpoint && session) {
|
|
64
|
+
llmApiKey =
|
|
65
|
+
llmApiKey ??
|
|
66
|
+
(await session.ask("LLM API key", { silent: true }));
|
|
67
|
+
llmModel =
|
|
68
|
+
llmModel ??
|
|
69
|
+
(await session.ask("LLM model", {
|
|
70
|
+
defaultValue: existing.llm?.model || ""
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
const payload = { accessKey, secretKey, region };
|
|
74
|
+
if (llmEndpoint) {
|
|
75
|
+
payload.llm = {
|
|
76
|
+
endpoint: llmEndpoint,
|
|
77
|
+
apiKey: llmApiKey || existing.llm?.apiKey,
|
|
78
|
+
model: llmModel || existing.llm?.model
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const file = (0, config_1.writeLocalConfig)(payload);
|
|
82
|
+
logger_1.ui.success(`Saved to ${file}`);
|
|
83
|
+
logger_1.ui.kv("accessKey", `${accessKey.slice(0, 4)}****`);
|
|
84
|
+
logger_1.ui.kv("secretKey", "********");
|
|
85
|
+
logger_1.ui.kv("region", region);
|
|
86
|
+
if (llmEndpoint) {
|
|
87
|
+
logger_1.ui.kv("llm.endpoint", llmEndpoint);
|
|
88
|
+
if (payload.llm.apiKey)
|
|
89
|
+
logger_1.ui.kv("llm.apiKey", "********");
|
|
90
|
+
if (payload.llm.model)
|
|
91
|
+
logger_1.ui.kv("llm.model", payload.llm.model);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
logger_1.ui.kv("llm", "(not configured)");
|
|
95
|
+
}
|
|
96
|
+
logger_1.ui.dim(`Tip: run \`emr-cli run "列出集群"\` to verify.`);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
session?.close();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|