@optima-chat/browser-cli 0.5.1 → 0.5.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/.claude/skills/browser/SKILL.md +203 -0
- package/dist/index.js +56 -394
- package/package.json +3 -2
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser
|
|
3
|
+
description: "Browser automation for web interactions. Use when user needs to open/navigate to websites, login to platforms, fill forms, post content, test websites, or operate on third-party platforms without APIs. Uses 'browser-cli' commands. Do NOT use for simple web scraping (use WebFetch) or tasks that have dedicated skills (e.g. scout, tiktok, instagram, shein, etc.)."
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
owner_repo: Optima-Chat/optima-browser-use
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 浏览器自动化 Skill
|
|
9
|
+
|
|
10
|
+
## 使用场景
|
|
11
|
+
|
|
12
|
+
- 用户要求打开网站或访问某个 URL(如"打开 github"、"open youtube")
|
|
13
|
+
- 用户要求登录平台、填写表单或发布内容
|
|
14
|
+
- 用户需要测试网站功能
|
|
15
|
+
- 用户需要操作没有 API 的第三方平台
|
|
16
|
+
- 用户要求自动化浏览器交互
|
|
17
|
+
|
|
18
|
+
**不适用场景:**
|
|
19
|
+
- 简单网页抓取 → 使用 WebFetch
|
|
20
|
+
- 已有专用 skill 的任务(如 scout、tiktok、instagram、shein 等)
|
|
21
|
+
|
|
22
|
+
## 脚本优先原则
|
|
23
|
+
|
|
24
|
+
每次执行浏览器任务前,**先检查是否有现成脚本**:
|
|
25
|
+
|
|
26
|
+
1. `browser-cli scripts` — 查看已有脚本(显示名称、描述、步骤)
|
|
27
|
+
2. 如果有匹配的脚本 → 直接用 `browser-cli runscript <name> --param ...`
|
|
28
|
+
3. 如果没有 → 手动用 browser-cli 命令完成任务
|
|
29
|
+
|
|
30
|
+
脚本执行零 token 消耗,比手动操作快 10 倍以上。
|
|
31
|
+
|
|
32
|
+
## 命令
|
|
33
|
+
|
|
34
|
+
### 会话管理
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
browser-cli launch [--url <url>] [--force] # 启动浏览器(--force 先关闭已有会话)
|
|
38
|
+
browser-cli close # 关闭浏览器并释放资源
|
|
39
|
+
browser-cli status # 查看当前会话状态
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 导航
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
browser-cli navigate <url> # 跳转到 URL
|
|
46
|
+
browser-cli back # 后退
|
|
47
|
+
browser-cli refresh # 刷新页面
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 交互
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
browser-cli click <index> # 按索引点击元素
|
|
54
|
+
browser-cli input <index> <text> [--no-clear] # 输入文本(--no-clear 追加而非替换)
|
|
55
|
+
browser-cli select <index> <option> # 选择下拉选项
|
|
56
|
+
browser-cli scroll <up|down> [--pages <n>] # 滚动页面(默认 1 页)
|
|
57
|
+
browser-cli keys <key> # 键盘操作(Enter、Tab 等)
|
|
58
|
+
browser-cli upload <index> <file...> # 上传文件到 file input(路径自动转换)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 页面检查
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
browser-cli dom # 获取当前 DOM 状态
|
|
65
|
+
browser-cli screenshot [file] # 截图(默认:/tmp/screenshot.png)
|
|
66
|
+
browser-cli search <pattern> # 搜索页面文本
|
|
67
|
+
browser-cli find <css_selector> # 通过 CSS 选择器查询
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 标签页管理
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
browser-cli tabs # 列出所有标签页
|
|
74
|
+
browser-cli switch <tab_id> # 切换标签页
|
|
75
|
+
browser-cli close-tab <tab_id> # 关闭标签页
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 脚本
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
browser-cli scripts # 列出所有脚本(含描述和步骤)
|
|
82
|
+
browser-cli script-code <name> # 查看脚本源码
|
|
83
|
+
browser-cli runscript <name> --param key=value [...] # 执行脚本(自动轮询到完成)
|
|
84
|
+
browser-cli runscript <name> --param key=value --no-poll # 执行但不等待
|
|
85
|
+
browser-cli script-task <task-id> # 查询执行状态
|
|
86
|
+
browser-cli submit-script --name <name> --code-file <path> # 提交新脚本(新建或覆盖)
|
|
87
|
+
browser-cli delete-script <name> # 删除脚本
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Profile 管理
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
browser-cli profiles # 列出浏览器 profile
|
|
94
|
+
browser-cli delete-profile <id> # 删除 profile
|
|
95
|
+
browser-cli launch --profile <id> # 用指定 profile 启动
|
|
96
|
+
browser-cli runscript <name> --profile <id> --param ... # 用指定 profile 执行脚本
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## DOM 索引格式
|
|
100
|
+
|
|
101
|
+
每个操作都会返回当前 DOM 状态,格式如下:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
[N]<tag attributes />
|
|
105
|
+
content
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- 索引号 (N) **不连续**(如 [7]、[20]、[30])— 这是正常的
|
|
109
|
+
- 每次操作后索引会**重新分配** — 务必使用最新的索引
|
|
110
|
+
- 使用这些索引来执行 `click`、`input` 和 `select` 命令
|
|
111
|
+
|
|
112
|
+
## 操作流程
|
|
113
|
+
|
|
114
|
+
### 流程一:有脚本(常见)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 1. 检查脚本
|
|
118
|
+
browser-cli scripts
|
|
119
|
+
|
|
120
|
+
# 2. 直接执行
|
|
121
|
+
browser-cli runscript xiaohongshu-image-note \
|
|
122
|
+
--param title="今日分享" \
|
|
123
|
+
--param body="内容..." \
|
|
124
|
+
--param 'images=["~/images/photo1.jpg", "~/images/photo2.jpg"]'
|
|
125
|
+
# 输出: ✓ Script completed
|
|
126
|
+
|
|
127
|
+
# 小红书评论
|
|
128
|
+
browser-cli runscript xiaohongshu-comment \
|
|
129
|
+
--param search_keyword="辞职做跨境电商" \
|
|
130
|
+
--param comment="写得好!"
|
|
131
|
+
|
|
132
|
+
# 小红书数据采集
|
|
133
|
+
browser-cli runscript xiaohongshu-data-collect
|
|
134
|
+
|
|
135
|
+
# Instagram 发帖
|
|
136
|
+
browser-cli runscript instagram-image-post \
|
|
137
|
+
--param 'images=["~/images/photo.jpg"]' \
|
|
138
|
+
--param body="Morning vibes ☕"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 流程二:无脚本(手动操作)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# 1. 启动浏览器
|
|
145
|
+
browser-cli launch --url https://example.com
|
|
146
|
+
|
|
147
|
+
# 2. 查看 DOM,找到要操作的元素
|
|
148
|
+
browser-cli dom
|
|
149
|
+
|
|
150
|
+
# 3. 操作元素(用 DOM 返回的索引)
|
|
151
|
+
browser-cli click 15
|
|
152
|
+
browser-cli input 22 "内容"
|
|
153
|
+
browser-cli keys Enter
|
|
154
|
+
|
|
155
|
+
# 4. 完成后关闭
|
|
156
|
+
browser-cli close
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 流程三:脚本失败(Fallback)
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# 1. 执行脚本
|
|
163
|
+
browser-cli runscript xiaohongshu-image-note --param ...
|
|
164
|
+
# 输出: ⚠ Script needs fallback
|
|
165
|
+
# → 显示已完成和剩余步骤
|
|
166
|
+
|
|
167
|
+
# 2. Session 仍然打开 — 用手动命令继续完成剩余步骤
|
|
168
|
+
browser-cli dom
|
|
169
|
+
browser-cli click 15
|
|
170
|
+
browser-cli input 22 "内容"
|
|
171
|
+
|
|
172
|
+
# 3. 完成后关闭
|
|
173
|
+
browser-cli close
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 文件路径
|
|
177
|
+
|
|
178
|
+
浏览器后端可以直接读取用户工作空间的文件。传文件路径时直接使用你看到的路径即可,后端会自动转换:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
~/images/photo.jpg → 自动转换 ✓
|
|
182
|
+
/home/aiuser/images/photo.jpg → 自动转换 ✓
|
|
183
|
+
/mnt/efs/{userId}/images/... → 自动转换 ✓
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 需要用户登录态的任务
|
|
187
|
+
|
|
188
|
+
当用户要求操作需要登录的平台(小红书、Instagram、微博、淘宝等社交/电商平台),在开始执行前先引导用户安装扩展:
|
|
189
|
+
|
|
190
|
+
"这个任务需要用到你在 [平台名] 的登录态。请安装 Optima Browser Operator Chrome 扩展,这样我可以直接用你已登录的浏览器来操作,不需要你提供密码。"
|
|
191
|
+
|
|
192
|
+
安装后用户回到聊天页面,扩展自动连接,直接执行任务。如果用户在该平台未登录,提示用户先在自己的 Chrome 里登录。
|
|
193
|
+
|
|
194
|
+
## 注意事项
|
|
195
|
+
|
|
196
|
+
- **脚本优先**:每次操作前先 `browser-cli scripts`,有脚本就用脚本
|
|
197
|
+
- **DOM 自动返回**:每个操作会自动返回 DOM 状态 — 无需单独调用 `browser-cli dom`
|
|
198
|
+
- **避免不必要的截图**:截图消耗 token,日常导航用 DOM 输出即可
|
|
199
|
+
- **5 分钟超时**:无操作 5 分钟后会话自动关闭
|
|
200
|
+
- **务必关闭**:操作完成后运行 `browser-cli close` 释放资源
|
|
201
|
+
- **login_required 处理**:如果 runscript 返回 `login_required` 错误,说明该平台未登录。如果用户已安装 Extension 且 Extension 已连接,用户的 Chrome 通常已经登录了——让用户在自己的 Chrome 里手动登录该平台,然后重新 `runscript`。如果没有 Extension(走 Chromium),用 `browser-cli launch --url <平台URL>` 引导用户登录
|
|
202
|
+
- **Fallback 后更新脚本**:如果脚本频繁失败,重新生成并 submit-script 覆盖
|
|
203
|
+
- **Extension 模式**:标记了 `REQUIRES_USER_SESSION=True` 的脚本会自动使用用户的真实 Chrome(通过 Optima Browser Operator 扩展),未标记的脚本走 Chromium。这由后端自动判断,无需手动指定
|
package/dist/index.js
CHANGED
|
@@ -155,11 +155,13 @@ async function requestBinary(path) {
|
|
|
155
155
|
return Buffer.from(arrayBuffer);
|
|
156
156
|
}
|
|
157
157
|
var BrowserAPI = class {
|
|
158
|
-
async createSession(url, profileId, force) {
|
|
158
|
+
async createSession(url, profileId, force, connector, ziniaoProfileId) {
|
|
159
159
|
const body = {};
|
|
160
160
|
if (url) body.url = url;
|
|
161
161
|
if (profileId) body.profile_id = profileId;
|
|
162
162
|
if (force) body.force = true;
|
|
163
|
+
if (connector) body.connector = connector;
|
|
164
|
+
if (ziniaoProfileId) body.ziniao_profile_id = ziniaoProfileId;
|
|
163
165
|
return request("/api/sessions", {
|
|
164
166
|
method: "POST",
|
|
165
167
|
body: JSON.stringify(body)
|
|
@@ -173,6 +175,18 @@ var BrowserAPI = class {
|
|
|
173
175
|
method: "DELETE"
|
|
174
176
|
});
|
|
175
177
|
}
|
|
178
|
+
async setZiNiaoConfig(company, username, password) {
|
|
179
|
+
return request("/api/user/ziniao-config", {
|
|
180
|
+
method: "PUT",
|
|
181
|
+
body: JSON.stringify({ company, username, password })
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async getZiNiaoConfig() {
|
|
185
|
+
return request("/api/user/ziniao-config");
|
|
186
|
+
}
|
|
187
|
+
async deleteZiNiaoConfig() {
|
|
188
|
+
return request("/api/user/ziniao-config", { method: "DELETE" });
|
|
189
|
+
}
|
|
176
190
|
async closeSession(sessionId) {
|
|
177
191
|
await request(`/api/sessions/${sessionId}`, { method: "DELETE" });
|
|
178
192
|
}
|
|
@@ -266,50 +280,6 @@ var BrowserAPI = class {
|
|
|
266
280
|
body: JSON.stringify({ index, file_paths: filePaths })
|
|
267
281
|
});
|
|
268
282
|
}
|
|
269
|
-
// Workflow methods
|
|
270
|
-
async saveWorkflow(sessionId, name, description) {
|
|
271
|
-
return request("/api/workflows", {
|
|
272
|
-
method: "POST",
|
|
273
|
-
body: JSON.stringify({ session_id: sessionId, name, description })
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
async listWorkflows(name) {
|
|
277
|
-
const query = name ? `?name=${encodeURIComponent(name)}` : "";
|
|
278
|
-
return request(`/api/workflows${query}`);
|
|
279
|
-
}
|
|
280
|
-
async getWorkflow(workflowId) {
|
|
281
|
-
return request(`/api/workflows/${workflowId}`);
|
|
282
|
-
}
|
|
283
|
-
async updateWorkflow(workflowId, updates) {
|
|
284
|
-
return request(`/api/workflows/${workflowId}`, {
|
|
285
|
-
method: "PUT",
|
|
286
|
-
body: JSON.stringify(updates)
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
async deleteWorkflow(workflowId) {
|
|
290
|
-
await request(`/api/workflows/${workflowId}`, { method: "DELETE" });
|
|
291
|
-
}
|
|
292
|
-
async runWorkflow(workflowId, params = {}, force) {
|
|
293
|
-
return request(`/api/workflows/${workflowId}/run`, {
|
|
294
|
-
method: "POST",
|
|
295
|
-
body: JSON.stringify({ params, force: !!force })
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
async getTaskStatus(taskId) {
|
|
299
|
-
return request(`/api/workflow-tasks/${taskId}`);
|
|
300
|
-
}
|
|
301
|
-
async provideInput(taskId, values, confirm) {
|
|
302
|
-
return request(`/api/workflow-tasks/${taskId}/input`, {
|
|
303
|
-
method: "POST",
|
|
304
|
-
body: JSON.stringify({ values: values ?? null, confirm: confirm ?? null })
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
async cancelTask(taskId) {
|
|
308
|
-
return request(`/api/workflow-tasks/${taskId}/cancel`, {
|
|
309
|
-
method: "POST",
|
|
310
|
-
body: JSON.stringify({})
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
283
|
// Script methods
|
|
314
284
|
async submitScript(name, code) {
|
|
315
285
|
return request(`/api/scripts`, {
|
|
@@ -445,67 +415,6 @@ function formatError(err) {
|
|
|
445
415
|
function formatTabsInline(tabs) {
|
|
446
416
|
return tabs.map((t) => `[${t.tab_id}] ${t.title}`).join(", ");
|
|
447
417
|
}
|
|
448
|
-
function formatWorkflowSaved(workflow) {
|
|
449
|
-
return `\u2713 Workflow '${workflow.name}' saved (id: ${workflow.id}, ${workflow.steps.length} steps)`;
|
|
450
|
-
}
|
|
451
|
-
function formatWorkflowList(workflows) {
|
|
452
|
-
if (workflows.length === 0) {
|
|
453
|
-
return "No workflows found.";
|
|
454
|
-
}
|
|
455
|
-
const lines = ["Workflows:", ""];
|
|
456
|
-
for (const wf of workflows) {
|
|
457
|
-
const desc = wf.description ? ` \u2014 ${wf.description}` : "";
|
|
458
|
-
lines.push(` ${wf.name} (${wf.step_count} steps)${desc}`);
|
|
459
|
-
}
|
|
460
|
-
return lines.join("\n");
|
|
461
|
-
}
|
|
462
|
-
function formatWorkflowInfo(workflow) {
|
|
463
|
-
const lines = [];
|
|
464
|
-
lines.push(`Workflow: ${workflow.name}`);
|
|
465
|
-
lines.push(`ID: ${workflow.id}`);
|
|
466
|
-
if (workflow.description) lines.push(`Description: ${workflow.description}`);
|
|
467
|
-
lines.push(`Steps: ${workflow.steps.length}`);
|
|
468
|
-
lines.push(`Created: ${workflow.created_at}`);
|
|
469
|
-
lines.push("");
|
|
470
|
-
if (workflow.input_schema.length > 0) {
|
|
471
|
-
lines.push("Input variables:");
|
|
472
|
-
for (const v of workflow.input_schema) {
|
|
473
|
-
const req = v.required ? " (required)" : "";
|
|
474
|
-
lines.push(` ${v.name}: ${v.type}${req}`);
|
|
475
|
-
}
|
|
476
|
-
lines.push("");
|
|
477
|
-
}
|
|
478
|
-
lines.push("Steps:");
|
|
479
|
-
for (let i = 0; i < workflow.steps.length; i++) {
|
|
480
|
-
const s = workflow.steps[i];
|
|
481
|
-
lines.push(` ${i + 1}. [${s.step_type}] ${s.description}`);
|
|
482
|
-
}
|
|
483
|
-
return lines.join("\n");
|
|
484
|
-
}
|
|
485
|
-
function formatWorkflowTask(task) {
|
|
486
|
-
const lines = [];
|
|
487
|
-
const name = task.workflow_name ? ` (${task.workflow_name})` : "";
|
|
488
|
-
lines.push(`Task: ${task.id}${name}`);
|
|
489
|
-
lines.push(`Status: ${task.status}`);
|
|
490
|
-
lines.push(`Progress: ${task.current_step}/${task.total_steps}`);
|
|
491
|
-
if (task.session_id) lines.push(`Session: ${task.session_id}`);
|
|
492
|
-
if (task.missing_vars && task.missing_vars.length > 0) {
|
|
493
|
-
lines.push(`Missing: ${task.missing_vars.join(", ")}`);
|
|
494
|
-
}
|
|
495
|
-
if (task.pending_confirmation) {
|
|
496
|
-
lines.push(`Confirmation: ${task.pending_confirmation}`);
|
|
497
|
-
}
|
|
498
|
-
if (task.error) lines.push(`Error: ${task.error}`);
|
|
499
|
-
if (task.output) {
|
|
500
|
-
const entries = Object.entries(task.output).filter(([k]) => !k.startsWith("_")).map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
501
|
-
if (entries) lines.push(`Output: ${entries}`);
|
|
502
|
-
}
|
|
503
|
-
if (task.completed_at) lines.push(`Completed: ${task.completed_at}`);
|
|
504
|
-
return lines.join("\n");
|
|
505
|
-
}
|
|
506
|
-
function formatWorkflowDeleted(name) {
|
|
507
|
-
return `\u2713 Workflow '${name}' deleted`;
|
|
508
|
-
}
|
|
509
418
|
function formatProfiles(result) {
|
|
510
419
|
if (result.profiles.length === 0) {
|
|
511
420
|
return "No browser profiles found.";
|
|
@@ -523,9 +432,9 @@ function formatProfileDelete(profileId) {
|
|
|
523
432
|
|
|
524
433
|
// src/commands/lifecycle.ts
|
|
525
434
|
function registerLifecycleCommands(program2) {
|
|
526
|
-
program2.command("launch").description("Launch a browser session").option("--url <url>", "Navigate to URL after launch").option("--profile <id>", "Use a named browser profile").option("--force", "Close existing session before launching").action(async (opts) => {
|
|
435
|
+
program2.command("launch").description("Launch a browser session").option("--url <url>", "Navigate to URL after launch").option("--profile <id>", "Use a named browser profile").option("--force", "Close existing session before launching").option("--connector <type>", "Browser connector: chrome or ziniao").option("--ziniao-profile <id>", "ZiNiao browser profile ID").action(async (opts) => {
|
|
527
436
|
try {
|
|
528
|
-
const state = await api.createSession(opts.url, opts.profile, opts.force);
|
|
437
|
+
const state = await api.createSession(opts.url, opts.profile, opts.force, opts.connector, opts.ziniaoProfile);
|
|
529
438
|
saveSessionId(state.session_id);
|
|
530
439
|
console.log(formatLaunch(state));
|
|
531
440
|
} catch (err) {
|
|
@@ -741,291 +650,6 @@ function registerTabCommands(program2) {
|
|
|
741
650
|
});
|
|
742
651
|
}
|
|
743
652
|
|
|
744
|
-
// src/commands/workflow.ts
|
|
745
|
-
function registerWorkflowCommands(program2) {
|
|
746
|
-
program2.command("save-workflow").argument("<name>", "Workflow name").description("Save current session actions as a workflow").option("--description <desc>", "Workflow description").action(async (name, opts) => {
|
|
747
|
-
try {
|
|
748
|
-
const sessionId = loadSessionId();
|
|
749
|
-
const workflow = await api.saveWorkflow(sessionId, name, opts.description);
|
|
750
|
-
console.log(formatWorkflowSaved(workflow));
|
|
751
|
-
} catch (err) {
|
|
752
|
-
console.error(formatError(err));
|
|
753
|
-
process.exit(1);
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
program2.command("list-workflows").description("List saved workflows").action(async () => {
|
|
757
|
-
try {
|
|
758
|
-
const workflows = await api.listWorkflows();
|
|
759
|
-
console.log(formatWorkflowList(workflows));
|
|
760
|
-
} catch (err) {
|
|
761
|
-
console.error(formatError(err));
|
|
762
|
-
process.exit(1);
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
program2.command("workflow-info").argument("<name>", "Workflow name").description("Show workflow details").action(async (name) => {
|
|
766
|
-
try {
|
|
767
|
-
const workflows = await api.listWorkflows(name);
|
|
768
|
-
if (workflows.length === 0) {
|
|
769
|
-
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
770
|
-
process.exit(1);
|
|
771
|
-
}
|
|
772
|
-
const workflow = await api.getWorkflow(workflows[0].id);
|
|
773
|
-
console.log(formatWorkflowInfo(workflow));
|
|
774
|
-
} catch (err) {
|
|
775
|
-
console.error(formatError(err));
|
|
776
|
-
process.exit(1);
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
program2.command("run-workflow").argument("<name>", "Workflow name").description("Execute a saved workflow").option("--param <params...>", "Parameters as key=value").option("--force", "Force run even if another task is active").action(async (name, opts) => {
|
|
780
|
-
try {
|
|
781
|
-
const workflows = await api.listWorkflows(name);
|
|
782
|
-
if (workflows.length === 0) {
|
|
783
|
-
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
784
|
-
process.exit(1);
|
|
785
|
-
}
|
|
786
|
-
const params = {};
|
|
787
|
-
if (opts.param) {
|
|
788
|
-
for (const p of opts.param) {
|
|
789
|
-
const [k, ...rest] = p.split("=");
|
|
790
|
-
params[k] = rest.join("=");
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
const task = await api.runWorkflow(workflows[0].id, params, opts.force);
|
|
794
|
-
console.log(formatWorkflowTask(task));
|
|
795
|
-
} catch (err) {
|
|
796
|
-
console.error(formatError(err));
|
|
797
|
-
process.exit(1);
|
|
798
|
-
}
|
|
799
|
-
});
|
|
800
|
-
program2.command("workflow-status").argument("<task_id>", "Task ID").description("Check workflow task status").action(async (taskId) => {
|
|
801
|
-
try {
|
|
802
|
-
const task = await api.getTaskStatus(taskId);
|
|
803
|
-
console.log(formatWorkflowTask(task));
|
|
804
|
-
} catch (err) {
|
|
805
|
-
console.error(formatError(err));
|
|
806
|
-
process.exit(1);
|
|
807
|
-
}
|
|
808
|
-
});
|
|
809
|
-
program2.command("workflow-cancel").argument("<task_id>", "Task ID").description("Cancel a running workflow task").action(async (taskId) => {
|
|
810
|
-
try {
|
|
811
|
-
const task = await api.cancelTask(taskId);
|
|
812
|
-
console.log(formatWorkflowTask(task));
|
|
813
|
-
} catch (err) {
|
|
814
|
-
console.error(formatError(err));
|
|
815
|
-
process.exit(1);
|
|
816
|
-
}
|
|
817
|
-
});
|
|
818
|
-
program2.command("workflow-input").argument("<task_id>", "Task ID").description("Provide input or confirmation to a waiting workflow task").option("--value <value>", "Provide a single value (only when one variable is missing)").option("--var <vars...>", "Provide named variables as key=value").option("--confirm", "Confirm a sensitive operation").action(async (taskId, opts) => {
|
|
819
|
-
try {
|
|
820
|
-
const values = {};
|
|
821
|
-
if (opts.value) {
|
|
822
|
-
const task = await api.getTaskStatus(taskId);
|
|
823
|
-
if (!task.missing_vars || task.missing_vars.length !== 1) {
|
|
824
|
-
console.error(formatError(new Error("--value can only be used when exactly one variable is missing. Use --var key=value instead.")));
|
|
825
|
-
process.exit(1);
|
|
826
|
-
}
|
|
827
|
-
values[task.missing_vars[0]] = opts.value;
|
|
828
|
-
}
|
|
829
|
-
if (opts.var) {
|
|
830
|
-
for (const v of opts.var) {
|
|
831
|
-
const [k, ...rest] = v.split("=");
|
|
832
|
-
values[k] = rest.join("=");
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
const hasValues = Object.keys(values).length > 0;
|
|
836
|
-
const result = await api.provideInput(taskId, hasValues ? values : void 0, opts.confirm);
|
|
837
|
-
console.log(formatWorkflowTask(result));
|
|
838
|
-
} catch (err) {
|
|
839
|
-
console.error(formatError(err));
|
|
840
|
-
process.exit(1);
|
|
841
|
-
}
|
|
842
|
-
});
|
|
843
|
-
program2.command("delete-workflow").argument("<name>", "Workflow name").description("Delete a saved workflow").action(async (name) => {
|
|
844
|
-
try {
|
|
845
|
-
const workflows = await api.listWorkflows(name);
|
|
846
|
-
if (workflows.length === 0) {
|
|
847
|
-
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
848
|
-
process.exit(1);
|
|
849
|
-
}
|
|
850
|
-
await api.deleteWorkflow(workflows[0].id);
|
|
851
|
-
console.log(formatWorkflowDeleted(name));
|
|
852
|
-
} catch (err) {
|
|
853
|
-
console.error(formatError(err));
|
|
854
|
-
process.exit(1);
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
program2.command("edit-workflow").argument("<name>", "Workflow name").description("Edit a workflow").option("--set-var", "Set a step value as variable").option("--step <n>", "Step number (1-based)", parseInt).option("--name <varName>", "Variable name").option("--add-input <varName>", "Add an input variable").option("--type <type>", "Variable type", "string").option("--required", "Mark variable as required").option("--add-step <type>", "Add a step (extract, wait_for_element)").option("--selectors <json>", "CSS selectors for extract step").option("--screenshot", "Include screenshot in extract step").option("--selector <css>", "CSS selector for wait_for_element step").option("--timeout <seconds>", "Timeout for wait_for_element step", parseInt).option("--remove-step <n>", "Remove step N (1-based)", parseInt).option("--update-step <n>", "Update step N (1-based)", parseInt).option("--requires-confirmation", "Mark step as requiring confirmation").option("--no-requires-confirmation", "Unmark step confirmation requirement").option("--target-text <text>", "Update step target text").option("--auto-clean", "Auto-clean redundant steps").action(async (name, opts) => {
|
|
858
|
-
try {
|
|
859
|
-
const workflows = await api.listWorkflows(name);
|
|
860
|
-
if (workflows.length === 0) {
|
|
861
|
-
console.error(formatError(new Error(`Workflow '${name}' not found`)));
|
|
862
|
-
process.exit(1);
|
|
863
|
-
}
|
|
864
|
-
const workflow = await api.getWorkflow(workflows[0].id);
|
|
865
|
-
if (opts.setVar && opts.step && opts.name) {
|
|
866
|
-
const stepIdx = opts.step - 1;
|
|
867
|
-
if (stepIdx < 0 || stepIdx >= workflow.steps.length) {
|
|
868
|
-
console.error(formatError(new Error(`Step ${opts.step} out of range`)));
|
|
869
|
-
process.exit(1);
|
|
870
|
-
}
|
|
871
|
-
const step = workflow.steps[stepIdx];
|
|
872
|
-
if (step.step_type === "navigate" && step.url) {
|
|
873
|
-
step.url = `{${opts.name}}`;
|
|
874
|
-
} else if (step.value !== void 0 && step.value !== null) {
|
|
875
|
-
step.value = `{${opts.name}}`;
|
|
876
|
-
} else {
|
|
877
|
-
console.error(formatError(new Error(`Step ${opts.step} ([${step.step_type}]) has no value or url to parameterize`)));
|
|
878
|
-
process.exit(1);
|
|
879
|
-
}
|
|
880
|
-
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
881
|
-
console.log(`\u2713 Step ${opts.step} value set to {${opts.name}}`);
|
|
882
|
-
} else if (opts.addInput) {
|
|
883
|
-
const inputVar = {
|
|
884
|
-
name: opts.addInput,
|
|
885
|
-
type: opts.type || "string",
|
|
886
|
-
description: "",
|
|
887
|
-
required: !!opts.required,
|
|
888
|
-
default: null,
|
|
889
|
-
sensitive: false
|
|
890
|
-
};
|
|
891
|
-
const schema = workflow.input_schema || [];
|
|
892
|
-
schema.push(inputVar);
|
|
893
|
-
await api.updateWorkflow(workflow.id, { input_schema: schema });
|
|
894
|
-
console.log(`\u2713 Input variable '${opts.addInput}' added`);
|
|
895
|
-
} else if (opts.addStep === "extract") {
|
|
896
|
-
const extractStep = {
|
|
897
|
-
step_type: "extract",
|
|
898
|
-
description: "Extract data",
|
|
899
|
-
extract_selectors: opts.selectors ? JSON.parse(opts.selectors) : {},
|
|
900
|
-
extract_screenshot: !!opts.screenshot
|
|
901
|
-
};
|
|
902
|
-
workflow.steps.push(extractStep);
|
|
903
|
-
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
904
|
-
console.log(`\u2713 Extract step added`);
|
|
905
|
-
} else if (opts.addStep === "wait_for_element") {
|
|
906
|
-
const waitStep = {
|
|
907
|
-
step_type: "wait_for_element",
|
|
908
|
-
description: `Wait for ${opts.selector || "element"}`,
|
|
909
|
-
wait_selector: opts.selector || "",
|
|
910
|
-
wait_timeout: opts.timeout || 30
|
|
911
|
-
};
|
|
912
|
-
workflow.steps.push(waitStep);
|
|
913
|
-
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
914
|
-
console.log(`\u2713 wait_for_element step added`);
|
|
915
|
-
} else if (opts.removeStep) {
|
|
916
|
-
const stepIdx = opts.removeStep - 1;
|
|
917
|
-
if (stepIdx < 0 || stepIdx >= workflow.steps.length) {
|
|
918
|
-
console.error(formatError(new Error(`Step ${opts.removeStep} out of range`)));
|
|
919
|
-
process.exit(1);
|
|
920
|
-
}
|
|
921
|
-
const removed = workflow.steps.splice(stepIdx, 1)[0];
|
|
922
|
-
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
923
|
-
console.log(`\u2713 Step ${opts.removeStep} removed ([${removed.step_type}] ${removed.description})`);
|
|
924
|
-
} else if (opts.updateStep) {
|
|
925
|
-
const stepIdx = opts.updateStep - 1;
|
|
926
|
-
if (stepIdx < 0 || stepIdx >= workflow.steps.length) {
|
|
927
|
-
console.error(formatError(new Error(`Step ${opts.updateStep} out of range`)));
|
|
928
|
-
process.exit(1);
|
|
929
|
-
}
|
|
930
|
-
const step = workflow.steps[stepIdx];
|
|
931
|
-
let changed = false;
|
|
932
|
-
if (opts.requiresConfirmation === true) {
|
|
933
|
-
step.requires_confirmation = true;
|
|
934
|
-
changed = true;
|
|
935
|
-
} else if (opts.requiresConfirmation === false) {
|
|
936
|
-
step.requires_confirmation = false;
|
|
937
|
-
changed = true;
|
|
938
|
-
}
|
|
939
|
-
if (opts.targetText) {
|
|
940
|
-
step.target_text = opts.targetText;
|
|
941
|
-
changed = true;
|
|
942
|
-
}
|
|
943
|
-
if (!changed) {
|
|
944
|
-
console.log("Specify what to update: --requires-confirmation, --target-text <text>");
|
|
945
|
-
process.exit(1);
|
|
946
|
-
}
|
|
947
|
-
await api.updateWorkflow(workflow.id, { steps: workflow.steps });
|
|
948
|
-
console.log(`\u2713 Step ${opts.updateStep} updated`);
|
|
949
|
-
} else if (opts.autoClean) {
|
|
950
|
-
const before = workflow.steps.length;
|
|
951
|
-
const cleaned = autoCleanSteps(workflow.steps);
|
|
952
|
-
await api.updateWorkflow(workflow.id, { steps: cleaned });
|
|
953
|
-
const removed = before - cleaned.length;
|
|
954
|
-
console.log(`\u2713 Auto-clean: ${removed} step(s) removed (${before} \u2192 ${cleaned.length})`);
|
|
955
|
-
} else {
|
|
956
|
-
console.log("Usage:");
|
|
957
|
-
console.log(" edit-workflow <name> --set-var --step N --name varName");
|
|
958
|
-
console.log(" edit-workflow <name> --add-input varName --type T [--required]");
|
|
959
|
-
console.log(" edit-workflow <name> --add-step extract --selectors '{...}' [--screenshot]");
|
|
960
|
-
console.log(" edit-workflow <name> --add-step wait_for_element --selector '.css' [--timeout 30]");
|
|
961
|
-
console.log(" edit-workflow <name> --remove-step N");
|
|
962
|
-
console.log(" edit-workflow <name> --update-step N --requires-confirmation");
|
|
963
|
-
console.log(" edit-workflow <name> --update-step N --target-text 'new text'");
|
|
964
|
-
console.log(" edit-workflow <name> --auto-clean");
|
|
965
|
-
}
|
|
966
|
-
} catch (err) {
|
|
967
|
-
console.error(formatError(err));
|
|
968
|
-
process.exit(1);
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
function autoCleanSteps(steps) {
|
|
973
|
-
let result = [...steps];
|
|
974
|
-
result = cleanNavigateBackNavigate(result);
|
|
975
|
-
result = cleanConsecutiveInput(result);
|
|
976
|
-
result = cleanConsecutiveScroll(result);
|
|
977
|
-
result = cleanConsecutiveClick(result);
|
|
978
|
-
return result;
|
|
979
|
-
}
|
|
980
|
-
function cleanNavigateBackNavigate(steps) {
|
|
981
|
-
const result = [];
|
|
982
|
-
let i = 0;
|
|
983
|
-
while (i < steps.length) {
|
|
984
|
-
if (i + 2 < steps.length && steps[i].step_type === "navigate" && steps[i + 1].step_type === "go_back" && steps[i + 2].step_type === "navigate") {
|
|
985
|
-
i += 2;
|
|
986
|
-
continue;
|
|
987
|
-
}
|
|
988
|
-
result.push(steps[i]);
|
|
989
|
-
i++;
|
|
990
|
-
}
|
|
991
|
-
return result;
|
|
992
|
-
}
|
|
993
|
-
function sameElement(a, b) {
|
|
994
|
-
return !!a.target_text && a.target_text === b.target_text && a.element_tag === b.element_tag;
|
|
995
|
-
}
|
|
996
|
-
function cleanConsecutiveInput(steps) {
|
|
997
|
-
const result = [];
|
|
998
|
-
for (let i = 0; i < steps.length; i++) {
|
|
999
|
-
if (steps[i].step_type === "input" && i + 1 < steps.length && steps[i + 1].step_type === "input" && sameElement(steps[i], steps[i + 1])) {
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1002
|
-
result.push(steps[i]);
|
|
1003
|
-
}
|
|
1004
|
-
return result;
|
|
1005
|
-
}
|
|
1006
|
-
function cleanConsecutiveScroll(steps) {
|
|
1007
|
-
const result = [];
|
|
1008
|
-
for (let i = 0; i < steps.length; i++) {
|
|
1009
|
-
if (steps[i].step_type === "scroll" && i + 1 < steps.length && steps[i + 1].step_type === "scroll" && steps[i].scroll_down === steps[i + 1].scroll_down) {
|
|
1010
|
-
const amount = (steps[i].scroll_amount || 1) + (steps[i + 1].scroll_amount || 1);
|
|
1011
|
-
steps[i + 1] = { ...steps[i + 1], scroll_amount: amount };
|
|
1012
|
-
continue;
|
|
1013
|
-
}
|
|
1014
|
-
result.push(steps[i]);
|
|
1015
|
-
}
|
|
1016
|
-
return result;
|
|
1017
|
-
}
|
|
1018
|
-
function cleanConsecutiveClick(steps) {
|
|
1019
|
-
const result = [];
|
|
1020
|
-
for (let i = 0; i < steps.length; i++) {
|
|
1021
|
-
if (steps[i].step_type === "click" && i + 1 < steps.length && steps[i + 1].step_type === "click" && sameElement(steps[i], steps[i + 1])) {
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
result.push(steps[i]);
|
|
1025
|
-
}
|
|
1026
|
-
return result;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
653
|
// src/commands/script.ts
|
|
1030
654
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1031
655
|
var POLL_INTERVAL_MS = 2e3;
|
|
@@ -1198,6 +822,44 @@ function registerProfileCommands(program2) {
|
|
|
1198
822
|
});
|
|
1199
823
|
}
|
|
1200
824
|
|
|
825
|
+
// src/commands/ziniao.ts
|
|
826
|
+
function registerZiNiaoCommands(program2) {
|
|
827
|
+
const ziniao = program2.command("ziniao").description("Manage ZiNiao browser credentials");
|
|
828
|
+
ziniao.command("set").description("Set ZiNiao credentials").requiredOption("--company <company>", "ZiNiao company name").requiredOption("--username <username>", "ZiNiao username").requiredOption("--password <password>", "ZiNiao password").action(async (opts) => {
|
|
829
|
+
try {
|
|
830
|
+
const result = await api.setZiNiaoConfig(opts.company, opts.username, opts.password);
|
|
831
|
+
console.log(`\u2713 ZiNiao credentials saved (company: ${result.company}, user: ${result.username})`);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
console.error(formatError(err));
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
ziniao.command("show").description("Show current ZiNiao config").action(async () => {
|
|
838
|
+
try {
|
|
839
|
+
const result = await api.getZiNiaoConfig();
|
|
840
|
+
if (!result.configured) {
|
|
841
|
+
console.log("No ZiNiao credentials configured. Use: browser-cli ziniao set --company ... --username ... --password ...");
|
|
842
|
+
} else {
|
|
843
|
+
console.log(`Company: ${result.company}`);
|
|
844
|
+
console.log(`Username: ${result.username}`);
|
|
845
|
+
console.log(`Password: ********`);
|
|
846
|
+
}
|
|
847
|
+
} catch (err) {
|
|
848
|
+
console.error(formatError(err));
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
ziniao.command("clear").description("Remove ZiNiao credentials").action(async () => {
|
|
853
|
+
try {
|
|
854
|
+
await api.deleteZiNiaoConfig();
|
|
855
|
+
console.log("\u2713 ZiNiao credentials removed");
|
|
856
|
+
} catch (err) {
|
|
857
|
+
console.error(formatError(err));
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
1201
863
|
// src/index.ts
|
|
1202
864
|
var require2 = createRequire(import.meta.url);
|
|
1203
865
|
var { version } = require2("../package.json");
|
|
@@ -1214,7 +876,7 @@ registerNavigationCommands(program);
|
|
|
1214
876
|
registerInteractionCommands(program);
|
|
1215
877
|
registerInfoCommands(program);
|
|
1216
878
|
registerTabCommands(program);
|
|
1217
|
-
registerWorkflowCommands(program);
|
|
1218
879
|
registerScriptCommands(program);
|
|
1219
880
|
registerProfileCommands(program);
|
|
881
|
+
registerZiNiaoCommands(program);
|
|
1220
882
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/browser-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"browser-cli": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
".claude"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsup",
|