uniner-cli 1.0.1 → 1.0.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.
package/README.md CHANGED
@@ -21,9 +21,13 @@ npm install -g uniner-cli
21
21
  安装过程:
22
22
 
23
23
  1. npm 装本壳包(约 50KB)
24
- 2. `postinstall` 钩子从 `static.uniner.com` 拉取约 250MB 便携包
25
- 3. 解压到 `%USERPROFILE%\.uniner-cli\<version>\`
26
- 4. 写入 `%USERPROFILE%\.claude\skills\uniner-cli\SKILL.md`
24
+ 2. `postinstall` 钩子根据当前平台从 `static.uniner.com` 拉取便携包:
25
+ - Windows: `uniner-cli-v<version>-win-x64.zip`
26
+ - Linux: `uniner-cli-v<version>-linux-x64.tar.gz`
27
+ 3. 解压到用户目录:
28
+ - Windows: `%USERPROFILE%\.uniner-cli\<version>\`
29
+ - Linux: `~/.uniner-cli/<version>/`
30
+ 4. 写入 Claude Code skill:`~/.claude/skills/uniner-cli/SKILL.md`
27
31
 
28
32
  完成后任意终端:
29
33
 
@@ -35,12 +39,16 @@ uniner-cli --help
35
39
 
36
40
  | 项 | 要求 |
37
41
  |---|---|
38
- | 操作系统 | 64 位 Windows 10 / 11 |
42
+ | 操作系统 | Windows 10/11 x64;Ubuntu 24.04+ x64 |
39
43
  | Node.js | >= 14 |
40
44
  | 磁盘空间 | ≥ 1.5 GB |
41
45
  | 网络 | 能访问 `static.uniner.com` |
42
46
 
43
- > Linux / macOS 支持在路上。
47
+ Linux 说明:
48
+ - Linux 包当前基于 Ubuntu 24.04 / glibc 2.39 构建和验证。
49
+ - 已验证模块:`tool` / `excel` / `skill` / `ddddocr OCR`。
50
+ - `web` 模块需要目标机安装 Chrome/Chromium。
51
+ - `desktop` 模块不支持 Linux。
44
52
 
45
53
  ## 使用示例
46
54
 
@@ -61,7 +69,9 @@ uniner-cli excel data write --file out.xlsx --cell B2 --value "hello"
61
69
  uniner-cli tool ocr ddddocr --img captcha.png
62
70
  ```
63
71
 
64
- 完整命令树(一级 + 二级 + 三级参数)见安装后 `%USERPROFILE%\.uniner-cli\<version>\skills\cli\SKILL.md`。
72
+ 完整命令树(一级 + 二级 + 三级参数)见安装后:
73
+ - Windows: `%USERPROFILE%\.uniner-cli\<version>\skills\cli\SKILL.md`
74
+ - Linux: `~/.uniner-cli/<version>/skills/cli/SKILL.md`
65
75
 
66
76
  ## 升级
67
77
 
@@ -8,6 +8,29 @@ description: RPA 自动化 CLI 工具,提供浏览器自动化、Excel 操作
8
8
  > 由 UninerCLI Installer v{{VERSION}} 自动安装到 `%USERPROFILE%\.claude\skills\uniner-cli\`
9
9
  > 安装位置(绝对路径):`{{INSTALL_PATH}}`
10
10
 
11
+ ## 🛑 任务结束硬性规则(最重要,先读这一节)
12
+
13
+ **每次用 uniner-cli 完成一个任务后,在你要说"任务完成"或写汇总表之前,必须先执行下面这段:**
14
+
15
+ 如果这次任务**满足任一条件**:
16
+
17
+ - 调用了 ≥ 3 个 uniner-cli 命令
18
+ - 涉及 web / desktop / OCR 任何一个模块
19
+ - 用户提到过"以后还会跑" / "每天都要" / "做成自动化"
20
+ - 任务里踩过坑(XPath 不稳、参数名错、Vue 组件、超时调整等)
21
+
22
+ → **必须主动问用户**:"这个流程涉及多步,要不要保存为可重放场景?以后 `uniner-cli skill run <name> --param ...` 就能 0 token 重放,可以放定时任务/RPA 平台里跑。要保存吗?"
23
+
24
+ 不要等用户开口,**主动**问。这是最有价值的一步——你刚刚花了上千 token 调通的流程,沉淀下来下次就能秒跑。
25
+
26
+ 如果用户说"好" / "可以" / "保存吧":按下面"场景沉淀(4 步流程)"章节执行。
27
+
28
+ 如果用户说"不用":直接结束,不再追问。
29
+
30
+ **违反这条规则 = 任务未完成。** 不管你跑得多顺,没问就是没问完整。
31
+
32
+ ---
33
+
11
34
  ## 何时使用本 Skill
12
35
 
13
36
  用户提到以下任一关键词或任务,**优先调用 uniner-cli**:
@@ -179,20 +202,9 @@ uniner-cli excel data write-range --file "{{INSTALL_PATH}}\workdir\out.xlsx" --c
179
202
  uniner-cli web browser close
180
203
  ```
181
204
 
182
- ## 场景沉淀(重要:完成任务后主动建议保存)
183
-
184
- 每完成一个完整 RPA 流程后,**主动建议**用户把它沉淀为可重放的场景。
185
- 之后用户用 `uniner-cli skill run <name>` 即可 **0 token 重放**——
186
- 不再需要 Claude,直接命令行就能跑、能放进定时任务 / Airflow / RPA 平台。
187
-
188
- ### 何时建议保存
205
+ ## 场景沉淀(4 步流程)
189
206
 
190
- 下面任一情况都该建议:
191
- - 任务跑完且至少调用了 ≥3 个 uniner-cli 命令
192
- - 任务里踩过坑(XPath 不稳、参数名错、Element UI 组件等)
193
- - 用户表达过 "以后还会跑"
194
-
195
- ### 怎么保存:3 个文件 + 4 步流程
207
+ > 这是顶部"任务结束硬性规则"提到的"4 步流程"。用户同意保存场景后,按这里走。
196
208
 
197
209
  每个场景一个目录,三件套:`meta.json` + `steps.json` + `SKILL.md`,存到:
198
210
 
@@ -326,20 +338,21 @@ Open Claude Code, paste this:
326
338
 
327
339
  客户复制这段给你 → 你 Read steps.json 调试 → 改完 `Write` 回去 → 让客户重跑。
328
340
 
329
- ### 给 Claude 的对话范式
330
-
331
- 完成任务后说类似的话(**主动询问,不等用户开口**):
341
+ ### 给 Claude 的对话范式(用户同意保存后)
332
342
 
333
- > "这个流程涉及 OCR、Vue 组件、XPath 等不稳定环节,我帮你保存为可重放场景吧?以后你直接 `uniner-cli skill run <name>` 就能 0 token 重放,还能放定时任务里跑。要保存吗?"
334
-
335
- 得到确认后:
343
+ 按这个顺序,**不要跳步**:
336
344
 
337
345
  1. 想一个 kebab-case 名字(如 `tms-export-waybills` / `sap-monthly-report`)
338
- 2. Bash 跑 `uniner-cli skill init <name>`
339
- 3. Write 写 meta.json(含动态参数 schema)+ steps.json(完整步骤 + 已知坑的避坑配置)+ SKILL.md(经验文档)
340
- 4. Bash 跑 `uniner-cli skill validate <name>`
346
+ 2. Bash 跑 `uniner-cli skill init <name>`(生成骨架)
347
+ 3. Write 写**真正的**:
348
+ - `meta.json`:含动态参数 schema(URL / USERNAME / PASSWORD / DATE_FROM 等)
349
+ - `steps.json`:把刚才跑过的命令序列**完整**写进去,含已知坑的避坑配置(retry / on_fail / always_run)
350
+ - `SKILL.md`:经验文档(已知坑、用法说明)
351
+ 4. Bash 跑 `uniner-cli skill validate <name>` 确认格式无误
341
352
  5. 让用户 `uniner-cli skill run <name> --param ...` 自己跑一次确认 0 token 重放可行
342
353
 
354
+ **关键**:steps.json 不是命令清单的注释——是**真正可执行的工作流**。要把 `${URL}` `${USERNAME}` `${ocrResult.data.result}` 这种变量绑好,让 `skill run` 真能跑通。
355
+
343
356
  ### 已沉淀的场景库
344
357
 
345
358
  ```bash
package/package.json CHANGED
@@ -1,46 +1,47 @@
1
- {
2
- "name": "uniner-cli",
3
- "version": "1.0.1",
4
- "description": "uniner-cli — RPA automation CLI: browser automation (Playwright/CDP), Excel, OCR, desktop/SAP, crypto/HTTP/email tools. Auto-installs a Claude Code skill on install.",
5
- "keywords": [
6
- "rpa",
7
- "automation",
8
- "uniner",
9
- "playwright",
10
- "excel",
11
- "ocr",
12
- "claude-code",
13
- "ai-agent"
14
- ],
15
- "homepage": "https://www.uniner.com/",
16
- "bugs": {
17
- "url": "https://www.uniner.com/",
18
- "email": "support@uniner.com"
19
- },
20
- "license": "Proprietary",
21
- "author": "Uniner Tech",
22
- "main": "lib/paths.js",
23
- "bin": {
24
- "uniner-cli": "bin/uniner-cli.js",
25
- "uniner-cli-uninstall": "bin/uniner-cli-uninstall.js"
26
- },
27
- "scripts": {
28
- "postinstall": "node scripts/postinstall.js"
29
- },
30
- "engines": {
31
- "node": "\u003e=14"
32
- },
33
- "os": [
34
- "win32"
35
- ],
36
- "cpu": [
37
- "x64"
38
- ],
39
- "files": [
40
- "bin/",
41
- "lib/",
42
- "scripts/",
43
- "README.md",
44
- "LICENSE"
45
- ]
46
- }
1
+ {
2
+ "name": "uniner-cli",
3
+ "version": "1.0.3",
4
+ "description": "uniner-cli — RPA automation CLI: browser automation (Playwright/CDP), Excel, OCR, desktop/SAP, crypto/HTTP/email tools. Auto-installs a Claude Code skill on install.",
5
+ "keywords": [
6
+ "rpa",
7
+ "automation",
8
+ "uniner",
9
+ "playwright",
10
+ "excel",
11
+ "ocr",
12
+ "claude-code",
13
+ "ai-agent"
14
+ ],
15
+ "homepage": "https://www.uniner.com/",
16
+ "bugs": {
17
+ "url": "https://www.uniner.com/",
18
+ "email": "support@uniner.com"
19
+ },
20
+ "license": "Proprietary",
21
+ "author": "Uniner Tech",
22
+ "main": "lib/paths.js",
23
+ "bin": {
24
+ "uniner-cli": "bin/uniner-cli.js",
25
+ "uniner-cli-uninstall": "bin/uniner-cli-uninstall.js"
26
+ },
27
+ "scripts": {
28
+ "postinstall": "node scripts/postinstall.js"
29
+ },
30
+ "engines": {
31
+ "node": ">=14"
32
+ },
33
+ "os": [
34
+ "win32",
35
+ "linux"
36
+ ],
37
+ "cpu": [
38
+ "x64"
39
+ ],
40
+ "files": [
41
+ "bin/",
42
+ "lib/",
43
+ "scripts/",
44
+ "README.md",
45
+ "LICENSE"
46
+ ]
47
+ }
@@ -3,12 +3,14 @@
3
3
  //
4
4
  // What it does:
5
5
  // 1. If the binary is already in place at ~/.uniner-cli/<version>/, exit (idempotent).
6
- // 2. Fetch latest.json from the OSS metadata URL to get portable_url + sha256.
7
- // 3. Stream-download the matching portable .zip into %TEMP%.
6
+ // 2. Fetch latest.json from the OSS metadata URL to get platform URL + sha256.
7
+ // 3. Stream-download the matching portable archive into temp dir.
8
+ // - Windows: .zip
9
+ // - Linux: .tar.gz
8
10
  // - Live progress every 2 seconds.
9
11
  // - Up to 3 retries with exponential backoff.
10
12
  // 4. Verify SHA-256.
11
- // 5. Extract via PowerShell's Expand-Archive (no native dep).
13
+ // 5. Extract via platform-native tools (PowerShell Expand-Archive / tar).
12
14
  // 6. Render SKILL.md.template with {{INSTALL_PATH}} / {{VERSION}} substituted
13
15
  // and write to ~/.claude/skills/uniner-cli/SKILL.md.
14
16
  // 7. Print "ready" and how to validate.
@@ -213,6 +215,58 @@ function extractZipPS(zipFile, destDir) {
213
215
  }
214
216
  }
215
217
 
218
+ function extractTarGz(tarFile, destDir) {
219
+ fs.mkdirSync(destDir, { recursive: true });
220
+
221
+ const result = spawnSync('tar', ['-xzf', tarFile, '-C', destDir], {
222
+ stdio: 'inherit',
223
+ });
224
+ if (result.error) {
225
+ fail(`Could not invoke tar to extract: ${result.error.message}`);
226
+ }
227
+ if (result.status !== 0) {
228
+ fail(`tar exited with status ${result.status}`);
229
+ }
230
+ }
231
+
232
+ function getPlatformInfo(meta) {
233
+ if (process.arch !== 'x64') {
234
+ fail(`Unsupported architecture: ${process.arch}. Only x64 is supported.`);
235
+ }
236
+
237
+ if (process.platform === 'win32') {
238
+ const info = meta && meta.windows && meta.windows.x64;
239
+ if (!info || !info.portable_url || !info.portable_sha256) {
240
+ fail('latest.json is missing windows.x64.portable_url / portable_sha256.');
241
+ }
242
+ return {
243
+ platformKey: 'windows.x64',
244
+ archiveUrl: info.portable_url,
245
+ archiveSha256: info.portable_sha256,
246
+ archiveSize: info.portable_size,
247
+ extract: extractZipPS,
248
+ expectedInnerBinary: path.join('bin', 'uniner-cli', 'uniner-cli.exe'),
249
+ };
250
+ }
251
+
252
+ if (process.platform === 'linux') {
253
+ const info = meta && meta.linux && meta.linux.x64;
254
+ if (!info || !info.portable_url || !info.portable_sha256) {
255
+ fail('latest.json is missing linux.x64.portable_url / portable_sha256.');
256
+ }
257
+ return {
258
+ platformKey: 'linux.x64',
259
+ archiveUrl: info.portable_url,
260
+ archiveSha256: info.portable_sha256,
261
+ archiveSize: info.portable_size,
262
+ extract: extractTarGz,
263
+ expectedInnerBinary: path.join('bin', 'uniner-cli', 'uniner-cli'),
264
+ };
265
+ }
266
+
267
+ fail(`Unsupported platform: ${process.platform}. Supported: win32-x64, linux-x64.`);
268
+ }
269
+
216
270
  // ----------------------------------------------------------------------
217
271
  // SKILL.md template render
218
272
  // ----------------------------------------------------------------------
@@ -265,16 +319,6 @@ async function main() {
265
319
  console.log(' uniner-cli postinstall');
266
320
  console.log('==========================================');
267
321
 
268
- // We only build a Windows binary right now. Refuse other platforms loudly
269
- // so users don't end up with a broken install.
270
- if (process.platform !== 'win32') {
271
- fail('Only Windows x64 is supported in this release. ' +
272
- 'Linux/macOS support is on the roadmap.');
273
- }
274
- if (process.arch !== 'x64') {
275
- fail(`Unsupported architecture: ${process.arch}. Only x64 is supported.`);
276
- }
277
-
278
322
  const version = pkg.version;
279
323
 
280
324
  // Skip when a working binary is already there. This makes
@@ -293,34 +337,30 @@ async function main() {
293
337
  log(` URL: ${METADATA_URL}`);
294
338
  const meta = await getJSON(METADATA_URL);
295
339
 
296
- const win = meta && meta.windows && meta.windows.x64;
297
- if (!win || !win.portable_url || !win.portable_sha256) {
298
- fail('latest.json is missing windows.x64.portable_url / portable_sha256. ' +
299
- 'The OSS metadata may be from an older release.');
300
- }
340
+ const platform = getPlatformInfo(meta);
301
341
  log(` Latest version: ${meta.version}`);
302
342
  log(` Package version (this npm release): ${version}`);
343
+ log(` Platform: ${platform.platformKey}`);
303
344
 
304
345
  if (meta.version !== version) {
305
346
  log(` [INFO] Server has ${meta.version} but this npm package is pinned ` +
306
- `to ${version}. Will fetch the package's own version artifacts ` +
307
- `from the OSS path that the URL implies.`);
347
+ `to ${version}. Will fetch the package artifacts from latest.json.`);
308
348
  }
309
349
 
310
350
  step('Downloading portable archive');
311
351
  const tmpDir = path.join(os.tmpdir(), 'uniner-cli-npm-install');
312
352
  fs.mkdirSync(tmpDir, { recursive: true });
313
- const zipPath = path.join(tmpDir, path.basename(win.portable_url));
353
+ const archivePath = path.join(tmpDir, path.basename(platform.archiveUrl));
314
354
 
315
- log(` From: ${win.portable_url}`);
316
- log(` To: ${zipPath}`);
317
- await downloadWithRetry(win.portable_url, zipPath, win.portable_size);
355
+ log(` From: ${platform.archiveUrl}`);
356
+ log(` To: ${archivePath}`);
357
+ await downloadWithRetry(platform.archiveUrl, archivePath, platform.archiveSize);
318
358
 
319
359
  step('Verifying SHA-256');
320
- const actual = await sha256OfFile(zipPath);
321
- if (actual.toLowerCase() !== win.portable_sha256.toLowerCase()) {
322
- try { fs.unlinkSync(zipPath); } catch (_) {}
323
- fail(`SHA-256 mismatch.\n expected: ${win.portable_sha256}\n got: ${actual}`);
360
+ const actual = await sha256OfFile(archivePath);
361
+ if (actual.toLowerCase() !== platform.archiveSha256.toLowerCase()) {
362
+ try { fs.unlinkSync(archivePath); } catch (_) {}
363
+ fail(`SHA-256 mismatch.\n expected: ${platform.archiveSha256}\n got: ${actual}`);
324
364
  }
325
365
  log(' Checksum verified.');
326
366
 
@@ -332,16 +372,17 @@ async function main() {
332
372
  log(' Removing previous half-extracted tree...');
333
373
  fs.rmSync(targetDir, { recursive: true, force: true });
334
374
  }
335
- extractZipPS(zipPath, targetDir);
375
+ platform.extract(archivePath, targetDir);
336
376
 
337
- // build_portable.bat puts everything inside an extra `uniner-cli-portable`
338
- // top-level folder. If we see exactly one subdir and the binary is one
339
- // level deeper, flatten so realBinaryPath() resolves cleanly.
377
+ // Portable archives put everything inside an extra top-level folder
378
+ // (`uniner-cli-portable` on Windows, `uniner-cli-linux-portable` on Linux).
379
+ // If we see exactly one subdir and the expected binary is one level deeper,
380
+ // flatten so realBinaryPath() resolves cleanly.
340
381
  const entries = fs.readdirSync(targetDir);
341
382
  if (entries.length === 1) {
342
383
  const inner = path.join(targetDir, entries[0]);
343
384
  if (fs.statSync(inner).isDirectory() &&
344
- fs.existsSync(path.join(inner, 'bin', 'uniner-cli', 'uniner-cli.exe'))) {
385
+ fs.existsSync(path.join(inner, platform.expectedInnerBinary))) {
345
386
  log(' Flattening extracted tree...');
346
387
  for (const item of fs.readdirSync(inner)) {
347
388
  fs.renameSync(path.join(inner, item), path.join(targetDir, item));
@@ -356,7 +397,7 @@ async function main() {
356
397
  }
357
398
 
358
399
  step('Cleaning up');
359
- try { fs.unlinkSync(zipPath); } catch (_) {}
400
+ try { fs.unlinkSync(archivePath); } catch (_) {}
360
401
  try { fs.rmdirSync(tmpDir); } catch (_) {}
361
402
  log(' Removed temporary files.');
362
403