@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
package/README.md CHANGED
@@ -1,335 +1,147 @@
1
- # volc-emr-agent
1
+ # emr-cli
2
2
 
3
- > 一个 **可控、可审计、可 dry-run** Volcengine EMR CLI Agent。
4
- >
5
- > Tool-first · Plan → Execute · LLM Planner(可选)· 真实 OpenAPI 调用 · 本地凭证 / 偏好记忆。
3
+ 一个面向 Volcengine EMR 的 CLI Agent。
6
4
 
7
- 你用一句自然语言描述任务(例如"列出所有运行中的集群"、"清理一周前已关停的集群"、"帮我建一个 3.7.0 Hadoop 集群"),Agent 会:
5
+ 它把自然语言任务拆成 `Plan -> Execute`,再调用真实 OpenAPI 执行,适合本地开发和命令行运维场景。
8
6
 
9
- 1. 由 LLM 生成一份结构化 `Plan`
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
- ## 已对接的 EMR OpenAPI(Version=2023-08-15)
15
+ ## 环境要求
18
16
 
19
- | Tool | OpenAPI Action | 风险级别 | 说明 |
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 # 注册 `volc-emr-agent` 到 PATH
34
+ npm link
35
+ emr-cli --help
46
36
  ```
47
37
 
48
- **环境要求**:Node.js ≥ 18
38
+ ## 快速开始
49
39
 
50
- ---
51
-
52
- ## 快速上手(三步)
40
+ 初始化配置:
53
41
 
54
42
  ```bash
55
- # 1) 初始化:填 AK/SK、地域,可选一起填 LLM
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
- # 交互式向导(支持同时配 LLM)
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 ark-xxx \
103
- --model doubao-seed-character-251128
51
+ --api-key <your-api-key> \
52
+ --model <your-model>
104
53
  ```
105
54
 
106
- ### 环境变量
55
+ 查看执行计划:
107
56
 
108
57
  ```bash
109
- export VOLC_ACCESSKEY=xxx
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
- # 查看已保存的本地配置(secrets 自动脱敏)
123
- volc-emr-agent config show
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
- ```bash
136
- volc-emr-agent run "列出所有运行中的集群"
137
- volc-emr-agent run "列出 cn-beijing 最近创建的 10 个 Hadoop 集群"
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
- # 批量执行(自动确认所有 high-risk 步骤)
147
- volc-emr-agent run "清理一周前已关停的集群" -y
87
+ 优先级:
148
88
 
149
- # 自定义天数
150
- volc-emr-agent run "清理 14 天前已关停的集群"
151
- ```
89
+ `CLI 参数 > 环境变量 > 本地配置文件`
152
90
 
153
- > 已被 EMR 归档(`marked archived`)的集群会被自动识别并**跳过**,不会让整个清理流程中断。
91
+ 默认配置文件:
154
92
 
155
- ### 创建集群(交互式向导)
93
+ - `~/.volc-emr/config.json`
94
+ - `~/.volc-emr/create-cluster-memory.json`
156
95
 
157
- ```bash
158
- volc-emr-agent run "帮我创建一个 3.7.0 的 Hadoop 集群"
159
- ```
96
+ 可通过 `VOLC_EMR_CONFIG_DIR` 修改配置目录。
160
97
 
161
- LLM 会先从你的描述里预填能猜到的字段(`ClusterType=Hadoop, ReleaseVersion=3.7.0`),Tool 接着启动向导:
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 # CLI 入口(commander)
113
+ ├── index.ts
223
114
  ├── agent/
224
- ├── agent.ts # 对外 Agent.run(task, options)
225
- ├── planner.ts # LLM-only Planner,无 LLM 时直接报错
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
- ├── base.ts # Tool 定义 (name/description/input/riskLevel/execute)
234
- │ ├── registry.ts # 聚合所有业务 Tool + API
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
- 所有业务能力都是 Tool(`name / description / input (Zod) / riskLevel / execute`)。LLM 看到的只有 Tool 目录(由 `zod-to-json-schema` 自动生成),**不能直接执行副作用**。
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
- ### 2. Plan → Execute 分离
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
- - [ ] 支持 `CreateCluster` 的 `--config file.json` 无交互模式(CI 友好)
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
 
@@ -3,16 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Agent = void 0;
4
4
  const planner_1 = require("./planner");
5
5
  const executor_1 = require("./executor");
6
+ const logger_1 = require("../runtime/logger");
6
7
  class Agent {
7
8
  constructor(ctx) {
8
9
  this.ctx = ctx;
9
10
  }
10
11
  async run(task, options = {}) {
12
+ logger_1.ui.section(`Task`);
13
+ logger_1.ui.info(task);
11
14
  const plan = await (0, planner_1.planTask)(task);
12
- console.log("\n📋 Plan:");
13
- console.log(plan);
14
- if (options.dryRun)
15
+ logger_1.ui.section(`Plan (${plan.length} step${plan.length > 1 ? "s" : ""})`);
16
+ plan.forEach((step, i) => {
17
+ logger_1.ui.raw(` ${i + 1}. ${step.tool}`);
18
+ if (step.input && Object.keys(step.input).length > 0) {
19
+ logger_1.ui.dim(` ${JSON.stringify(step.input)}`);
20
+ }
21
+ });
22
+ if (options.dryRun) {
23
+ logger_1.ui.divider();
24
+ logger_1.ui.info("dry-run mode: no Tool will be executed");
15
25
  return;
26
+ }
16
27
  await (0, executor_1.executePlan)(plan, this.ctx, { autoApprove: options.autoApprove });
17
28
  }
18
29
  }
@@ -4,22 +4,69 @@ 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 = {}) {
8
- for (const [i, step] of plan.entries()) {
9
- const tool = registry_1.toolList.find((t) => t.name === step.tool);
10
- if (!tool)
24
+ ctx.memory["__lastToolName"] = undefined;
25
+ ctx.memory["__lastToolResult"] = undefined;
26
+ logger_1.ui.section(`Execute (${plan.length} step${plan.length > 1 ? "s" : ""})`);
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);
30
+ if (!tool) {
31
+ (0, logger_1.logStep)(i, "fail", step.tool, { error: "Tool not found" });
11
32
  throw new Error(`Tool not found: ${step.tool}`);
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
+ }
12
49
  if (tool.riskLevel === "high") {
13
50
  const ok = options.autoApprove
14
51
  ? true
15
- : await (0, confirm_1.confirm)(`⚠️ 执行 ${tool.name} (input=${JSON.stringify(step.input)})?`);
52
+ : await (0, confirm_1.confirm)(tool.formatRiskConfirmMessage?.(step.input || {}) || `⚠ 执行高风险 Tool: ${tool.name} ${JSON.stringify(step.input)}`);
16
53
  if (!ok) {
17
- (0, logger_1.logStep)(i, `Skipped ${tool.name}`);
54
+ (0, logger_1.logStep)(i, "skip", tool.name, { reason: "user cancelled" });
18
55
  continue;
19
56
  }
20
57
  }
21
- (0, logger_1.logStep)(i, `Running ${tool.name}`, step.input);
22
- const result = await tool.execute(step.input, ctx);
23
- (0, logger_1.logStep)(i, `Done ${tool.name}`, result);
58
+ (0, logger_1.logStep)(i, "run", tool.name, { input: step.input });
59
+ try {
60
+ const result = await tool.execute(step.input, ctx);
61
+ ctx.memory["__lastToolName"] = tool.name;
62
+ ctx.memory["__lastToolResult"] = result;
63
+ (0, logger_1.logStep)(i, "done", tool.name, { output: result });
64
+ }
65
+ catch (err) {
66
+ (0, logger_1.logStep)(i, "fail", tool.name, { error: err?.message || String(err) });
67
+ throw err;
68
+ }
24
69
  }
70
+ logger_1.ui.divider();
71
+ logger_1.ui.success("Execute finished");
25
72
  }
@@ -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
- const validated = validatePlanAgainstTools(steps);
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
+ }