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 +16 -6
- package/lib/SKILL.md.template +35 -22
- package/package.json +47 -46
- package/scripts/postinstall.js +75 -34
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`
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
| 操作系统 |
|
|
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
|
-
|
|
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
|
-
完整命令树(一级 + 二级 +
|
|
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
|
|
package/lib/SKILL.md.template
CHANGED
|
@@ -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
|
-
|
|
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
|
|
340
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
|
7
|
-
// 3. Stream-download the matching portable
|
|
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
|
|
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
|
|
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
|
|
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
|
|
353
|
+
const archivePath = path.join(tmpDir, path.basename(platform.archiveUrl));
|
|
314
354
|
|
|
315
|
-
log(` From: ${
|
|
316
|
-
log(` To: ${
|
|
317
|
-
await downloadWithRetry(
|
|
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(
|
|
321
|
-
if (actual.toLowerCase() !==
|
|
322
|
-
try { fs.unlinkSync(
|
|
323
|
-
fail(`SHA-256 mismatch.\n expected: ${
|
|
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
|
-
|
|
375
|
+
platform.extract(archivePath, targetDir);
|
|
336
376
|
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
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,
|
|
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(
|
|
400
|
+
try { fs.unlinkSync(archivePath); } catch (_) {}
|
|
360
401
|
try { fs.rmdirSync(tmpDir); } catch (_) {}
|
|
361
402
|
log(' Removed temporary files.');
|
|
362
403
|
|