@optima-chat/browser-cli 0.5.1 → 0.5.3

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.
@@ -0,0 +1,245 @@
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
+ | 模式 | 触发条件 | 说明 |
37
+ |------|----------|------|
38
+ | **Desktop + 紫鸟** | 用户安装了 Desktop app 且配置了紫鸟 | 通过紫鸟反指纹浏览器操作,保留多店铺登录态 |
39
+ | **Desktop + Chrome** | 用户安装了 Desktop app | 通过用户本地 Chrome 操作 |
40
+ | **Cloud** | 无 Desktop app | 后端 headless Chromium,无登录态 |
41
+
42
+ **自动路由**:默认 `browser-cli launch` 会自动选择。如果用户安装了 Optima Browser Operator 桌面应用并在线,自动走 Desktop 模式。
43
+
44
+ ### 紫鸟浏览器(跨境电商多店铺管理)
45
+
46
+ 当用户提到紫鸟、多店铺、反指纹浏览器、或需要操作特定店铺时:
47
+
48
+ ```bash
49
+ # 设置紫鸟凭证(首次使用)
50
+ browser-cli ziniao set --company "公司名" --username "用户名" --password "密码"
51
+
52
+ # 查看紫鸟配置
53
+ browser-cli ziniao show
54
+
55
+ # 使用紫鸟指定店铺启动浏览器
56
+ browser-cli launch --connector ziniao --ziniao-profile <profile_id> --url <url>
57
+
58
+ # 普通启动(自动选择模式)
59
+ browser-cli launch --url <url>
60
+ ```
61
+
62
+ **紫鸟 profile_id** 需要从 Desktop app 的 capabilities 获取,或者用户直接提供。
63
+
64
+ ## 命令
65
+
66
+ ### 会话管理
67
+
68
+ ```bash
69
+ browser-cli launch [--url <url>] [--force] # 启动浏览器(自动选择模式)
70
+ browser-cli launch --connector ziniao --ziniao-profile <id> [--url <url>] # 指定紫鸟店铺
71
+ browser-cli launch --connector chrome [--url <url>] # 指定 Chrome
72
+ browser-cli close # 关闭浏览器并释放资源
73
+ browser-cli status # 查看当前会话状态
74
+ ```
75
+
76
+ ### 导航
77
+
78
+ ```bash
79
+ browser-cli navigate <url> # 跳转到 URL
80
+ browser-cli back # 后退
81
+ browser-cli refresh # 刷新页面
82
+ ```
83
+
84
+ ### 交互
85
+
86
+ ```bash
87
+ browser-cli click <index> # 按索引点击元素
88
+ browser-cli input <index> <text> [--no-clear] # 输入文本(--no-clear 追加而非替换)
89
+ browser-cli select <index> <option> # 选择下拉选项
90
+ browser-cli scroll <up|down> [--pages <n>] # 滚动页面(默认 1 页)
91
+ browser-cli keys <key> # 键盘操作(Enter、Tab 等)
92
+ browser-cli upload <index> <file...> # 上传文件到 file input(路径自动转换)
93
+ ```
94
+
95
+ ### 页面检查
96
+
97
+ ```bash
98
+ browser-cli dom # 获取当前 DOM 状态
99
+ browser-cli screenshot [file] # 截图(默认:/tmp/screenshot.png)
100
+ browser-cli search <pattern> # 搜索页面文本
101
+ browser-cli find <css_selector> # 通过 CSS 选择器查询
102
+ ```
103
+
104
+ ### 标签页管理
105
+
106
+ ```bash
107
+ browser-cli tabs # 列出所有标签页
108
+ browser-cli switch <tab_id> # 切换标签页
109
+ browser-cli close-tab <tab_id> # 关闭标签页
110
+ ```
111
+
112
+ ### 脚本
113
+
114
+ ```bash
115
+ browser-cli scripts # 列出所有脚本(含描述和步骤)
116
+ browser-cli script-code <name> # 查看脚本源码
117
+ browser-cli runscript <name> --param key=value [...] # 执行脚本(自动轮询到完成)
118
+ browser-cli runscript <name> --param key=value --no-poll # 执行但不等待
119
+ browser-cli script-task <task-id> # 查询执行状态
120
+ browser-cli submit-script --name <name> --code-file <path> # 提交新脚本(新建或覆盖)
121
+ browser-cli delete-script <name> # 删除脚本
122
+ ```
123
+
124
+ ### Profile 管理
125
+
126
+ ```bash
127
+ browser-cli profiles # 列出浏览器 profile
128
+ browser-cli delete-profile <id> # 删除 profile
129
+ browser-cli launch --profile <id> # 用指定 profile 启动
130
+ browser-cli runscript <name> --profile <id> --param ... # 用指定 profile 执行脚本
131
+ ```
132
+
133
+ ## DOM 索引格式
134
+
135
+ 每个操作都会返回当前 DOM 状态,格式如下:
136
+
137
+ ```
138
+ [N]<tag attributes />
139
+ content
140
+ ```
141
+
142
+ - 索引号 (N) **不连续**(如 [7]、[20]、[30])— 这是正常的
143
+ - 每次操作后索引会**重新分配** — 务必使用最新的索引
144
+ - 使用这些索引来执行 `click`、`input` 和 `select` 命令
145
+
146
+ ## 操作流程
147
+
148
+ ### 流程一:有脚本(常见)
149
+
150
+ ```bash
151
+ # 1. 检查脚本
152
+ browser-cli scripts
153
+
154
+ # 2. 直接执行
155
+ browser-cli runscript xiaohongshu-image-note \
156
+ --param title="今日分享" \
157
+ --param body="内容..." \
158
+ --param 'images=["~/images/photo1.jpg", "~/images/photo2.jpg"]'
159
+ # 输出: ✓ Script completed
160
+
161
+ # 小红书评论
162
+ browser-cli runscript xiaohongshu-comment \
163
+ --param search_keyword="辞职做跨境电商" \
164
+ --param comment="写得好!"
165
+
166
+ # 小红书数据采集
167
+ browser-cli runscript xiaohongshu-data-collect
168
+
169
+ # Instagram 发帖
170
+ browser-cli runscript instagram-image-post \
171
+ --param 'images=["~/images/photo.jpg"]' \
172
+ --param body="Morning vibes ☕"
173
+ ```
174
+
175
+ ### 流程二:无脚本(手动操作)
176
+
177
+ ```bash
178
+ # 1. 启动浏览器
179
+ browser-cli launch --url https://example.com
180
+
181
+ # 2. 查看 DOM,找到要操作的元素
182
+ browser-cli dom
183
+
184
+ # 3. 操作元素(用 DOM 返回的索引)
185
+ browser-cli click 15
186
+ browser-cli input 22 "内容"
187
+ browser-cli keys Enter
188
+
189
+ # 4. 完成后关闭
190
+ browser-cli close
191
+ ```
192
+
193
+ ### 流程三:脚本失败(Fallback)
194
+
195
+ ```bash
196
+ # 1. 执行脚本
197
+ browser-cli runscript xiaohongshu-image-note --param ...
198
+ # 输出: ⚠ Script needs fallback
199
+ # → 显示已完成和剩余步骤
200
+
201
+ # 2. Session 仍然打开 — 用手动命令继续完成剩余步骤
202
+ browser-cli dom
203
+ browser-cli click 15
204
+ browser-cli input 22 "内容"
205
+
206
+ # 3. 完成后关闭
207
+ browser-cli close
208
+ ```
209
+
210
+ ## 文件路径
211
+
212
+ 浏览器后端可以直接读取用户工作空间的文件。传文件路径时直接使用你看到的路径即可,后端会自动转换:
213
+
214
+ ```
215
+ ~/images/photo.jpg → 自动转换 ✓
216
+ /home/aiuser/images/photo.jpg → 自动转换 ✓
217
+ /mnt/efs/{userId}/images/... → 自动转换 ✓
218
+ ```
219
+
220
+ ## 需要用户登录态的任务
221
+
222
+ 当用户要求操作需要登录的平台(TikTok Shop、小红书、Instagram 等),需要用户的真实浏览器(有登录态)。
223
+
224
+ **推荐方案:Optima Browser Operator 桌面应用**
225
+
226
+ 1. 用户下载安装桌面应用(Mac .dmg / Windows .exe):https://github.com/Optima-Chat/optima-browser-use/releases
227
+ 2. 首次启动自动打开浏览器登录授权
228
+ 3. 授权后自动连接,后续 `browser-cli launch` 自动走 Desktop 模式
229
+
230
+ **跨境电商用户(多店铺)**:推荐安装紫鸟浏览器 + Desktop app,可以按店铺 profile 分别操作,每个店铺独立登录态。
231
+
232
+ **备选方案:Chrome 扩展**
233
+
234
+ 安装 Optima Browser Operator Chrome 扩展,直接用 Chrome 已登录的账号操作。
235
+
236
+ ## 注意事项
237
+
238
+ - **脚本优先**:每次操作前先 `browser-cli scripts`,有脚本就用脚本
239
+ - **DOM 自动返回**:每个操作会自动返回 DOM 状态 — 无需单独调用 `browser-cli dom`
240
+ - **避免不必要的截图**:截图消耗 token,日常导航用 DOM 输出即可
241
+ - **5 分钟超时**:无操作 5 分钟后会话自动关闭
242
+ - **务必关闭**:操作完成后运行 `browser-cli close` 释放资源
243
+ - **login_required 处理**:如果 runscript 返回 `login_required` 错误,说明该平台未登录。如果用户已安装 Extension 且 Extension 已连接,用户的 Chrome 通常已经登录了——让用户在自己的 Chrome 里手动登录该平台,然后重新 `runscript`。如果没有 Extension(走 Chromium),用 `browser-cli launch --url <平台URL>` 引导用户登录
244
+ - **Fallback 后更新脚本**:如果脚本频繁失败,重新生成并 submit-script 覆盖
245
+ - **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.1",
3
+ "version": "0.5.3",
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",