uniner-cli 1.0.0 → 1.0.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/bin/uniner-cli-uninstall.js +60 -2
- package/lib/SKILL.md.template +184 -0
- package/package.json +46 -46
- package/scripts/postinstall.js +20 -0
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
rootDir,
|
|
22
22
|
versionDir,
|
|
23
23
|
skillDir,
|
|
24
|
+
skillFile,
|
|
24
25
|
} = require('../lib/paths');
|
|
25
26
|
|
|
26
27
|
function log(msg) { console.log(`[uniner-cli] ${msg}`); }
|
|
@@ -40,6 +41,20 @@ function rmRf(p) {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function rmFile(p) {
|
|
45
|
+
try {
|
|
46
|
+
if (fs.existsSync(p)) {
|
|
47
|
+
fs.unlinkSync(p);
|
|
48
|
+
log(`Removed ${p}`);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
warn(`Could not remove ${p}: ${e.message}`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
43
58
|
function main() {
|
|
44
59
|
console.log('');
|
|
45
60
|
console.log('==========================================');
|
|
@@ -71,14 +86,57 @@ function main() {
|
|
|
71
86
|
warn(`Could not inspect ${rootDir()}: ${e.message}`);
|
|
72
87
|
}
|
|
73
88
|
|
|
74
|
-
// 3. Remove the
|
|
75
|
-
|
|
89
|
+
// 3. Remove ONLY the SKILL.md file. NEVER rm -rf the whole skill dir,
|
|
90
|
+
// because the user's accumulated scenarios/<name>/ subdirectories
|
|
91
|
+
// live there and represent real work they've sedimented over time.
|
|
92
|
+
// If they really want everything gone they can do it manually; we
|
|
93
|
+
// surface the path below so they know where to look.
|
|
94
|
+
const dir = skillDir();
|
|
95
|
+
if (rmFile(skillFile())) didSomething = true;
|
|
96
|
+
|
|
97
|
+
// 4. Look for scenarios/ — if there are any, keep them and tell the user.
|
|
98
|
+
let scenariosKept = 0;
|
|
99
|
+
const scenariosDir = path.join(dir, 'scenarios');
|
|
100
|
+
if (fs.existsSync(scenariosDir)) {
|
|
101
|
+
try {
|
|
102
|
+
scenariosKept = fs.readdirSync(scenariosDir).filter(n => {
|
|
103
|
+
const meta = path.join(scenariosDir, n, 'meta.json');
|
|
104
|
+
return fs.existsSync(meta);
|
|
105
|
+
}).length;
|
|
106
|
+
} catch (_) {
|
|
107
|
+
// best-effort
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 5. If skill dir is now genuinely empty, remove it. Otherwise keep it.
|
|
112
|
+
try {
|
|
113
|
+
if (fs.existsSync(dir)) {
|
|
114
|
+
const remaining = fs.readdirSync(dir);
|
|
115
|
+
if (remaining.length === 0) {
|
|
116
|
+
fs.rmdirSync(dir);
|
|
117
|
+
log(`Removed empty ${dir}`);
|
|
118
|
+
didSomething = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (_) {
|
|
122
|
+
// best-effort
|
|
123
|
+
}
|
|
76
124
|
|
|
77
125
|
if (!didSomething) {
|
|
78
126
|
log('Nothing to clean — already removed.');
|
|
79
127
|
}
|
|
80
128
|
|
|
81
129
|
console.log('');
|
|
130
|
+
if (scenariosKept > 0) {
|
|
131
|
+
console.log('==========================================');
|
|
132
|
+
console.log(` ${scenariosKept} scenario(s) PRESERVED at:`);
|
|
133
|
+
console.log(` ${scenariosDir}`);
|
|
134
|
+
console.log(' (your accumulated knowledge — not deleted)');
|
|
135
|
+
console.log(' To delete them too:');
|
|
136
|
+
console.log(` rm -rf "${scenariosDir}" # bash`);
|
|
137
|
+
console.log(` Remove-Item "${scenariosDir}" -Recurse -Force # PowerShell`);
|
|
138
|
+
console.log('==========================================');
|
|
139
|
+
}
|
|
82
140
|
console.log('Cleanup done. To complete uninstall, also run:');
|
|
83
141
|
console.log(' npm uninstall -g uniner-cli');
|
|
84
142
|
console.log('');
|
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,6 +202,167 @@ uniner-cli excel data write-range --file "{{INSTALL_PATH}}\workdir\out.xlsx" --c
|
|
|
179
202
|
uniner-cli web browser close
|
|
180
203
|
```
|
|
181
204
|
|
|
205
|
+
## 场景沉淀(4 步流程)
|
|
206
|
+
|
|
207
|
+
> 这是顶部"任务结束硬性规则"提到的"4 步流程"。用户同意保存场景后,按这里走。
|
|
208
|
+
|
|
209
|
+
每个场景一个目录,三件套:`meta.json` + `steps.json` + `SKILL.md`,存到:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
%USERPROFILE%\.claude\skills\uniner-cli\scenarios\<kebab-case-name>\
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**4 步标准流程**:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Step 1: 生成骨架(自动创建目录和模板文件)
|
|
219
|
+
uniner-cli skill init <name>
|
|
220
|
+
|
|
221
|
+
# Step 2: 用 Write 工具改 meta.json / steps.json / SKILL.md(见下方 schema)
|
|
222
|
+
|
|
223
|
+
# Step 3: 校验
|
|
224
|
+
uniner-cli skill validate <name>
|
|
225
|
+
|
|
226
|
+
# Step 4: 试跑一次
|
|
227
|
+
uniner-cli skill run <name> --param ...
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### meta.json schema
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"name": "<kebab-case-name>",
|
|
235
|
+
"version": "1.0",
|
|
236
|
+
"description": "一句话描述(会被用来匹配触发关键词)",
|
|
237
|
+
"params": {
|
|
238
|
+
"URL": { "type": "string", "required": true, "description": "登录页地址" },
|
|
239
|
+
"USERNAME": { "type": "string", "required": true, "description": "账号" },
|
|
240
|
+
"PASSWORD": { "type": "string", "required": true, "sensitive": true, "description": "密码" },
|
|
241
|
+
"DATE_FROM": { "type": "string", "required": false, "default": "today", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### steps.json schema
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"version": "1.0",
|
|
251
|
+
"name": "<同 meta.name>",
|
|
252
|
+
"steps": [
|
|
253
|
+
{
|
|
254
|
+
"id": "open-browser",
|
|
255
|
+
"cmd": "web browser open",
|
|
256
|
+
"args": { "url": "${URL}" },
|
|
257
|
+
"timeout_ms": 10000
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"id": "ocr-captcha",
|
|
261
|
+
"cmd": "tool ocr ddddocr",
|
|
262
|
+
"args": { "img": "workdir/captcha.png" },
|
|
263
|
+
"store": "ocrResult",
|
|
264
|
+
"comment": "整个 stdout JSON 存到 ocrResult,下面用 ${ocrResult.data.result} 取"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"id": "input-captcha",
|
|
268
|
+
"cmd": "web element input",
|
|
269
|
+
"args": { "xpath": "//input[@placeholder='验证码']", "value": "${ocrResult.data.result}" }
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"id": "close-browser",
|
|
273
|
+
"cmd": "web browser close",
|
|
274
|
+
"always_run": true,
|
|
275
|
+
"comment": "无论前面成功与否都跑(清理用)"
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 每个 step 支持的字段
|
|
282
|
+
|
|
283
|
+
| 字段 | 必需 | 说明 |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `id` | ✅ | 步骤 ID(唯一) |
|
|
286
|
+
| `cmd` | ✅ | uniner-cli 子命令,**不带 `uniner-cli` 前缀**,如 `web element input` |
|
|
287
|
+
| `args` | ✅ | dict 形式参数,对应 CLI 的 `--key value` |
|
|
288
|
+
| `store` | ❌ | 把 stdout JSON 整个存到变量名 |
|
|
289
|
+
| `if` | ❌ | 条件表达式(见下) |
|
|
290
|
+
| `on_fail` | ❌ | `abort`(默认)/ `continue` / `goto:<step_id>` |
|
|
291
|
+
| `retry` | ❌ | 失败重试次数(不含首次) |
|
|
292
|
+
| `retry_delay_ms` | ❌ | 重试间隔(默认 1000) |
|
|
293
|
+
| `timeout_ms` | ❌ | 单步超时(覆盖全局) |
|
|
294
|
+
| `always_run` | ❌ | 即使前面失败也跑(清理用) |
|
|
295
|
+
| `comment` | ❌ | 给人/AI 看的注释 |
|
|
296
|
+
|
|
297
|
+
### 变量替换语法
|
|
298
|
+
|
|
299
|
+
| 写法 | 含义 |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `${URL}` | 用户传入的 `--param URL=xxx` |
|
|
302
|
+
| `${ocrResult.data.result}` | 上一步 store 的 dict 字段(点号路径) |
|
|
303
|
+
| `${tableData.0.1}` | list 索引(0-based) |
|
|
304
|
+
| `${env:USERPROFILE}` | 环境变量 |
|
|
305
|
+
|
|
306
|
+
### 条件表达式语法(白名单)
|
|
307
|
+
|
|
308
|
+
| 写法 | 含义 |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `${var}` | 真值判断 |
|
|
311
|
+
| `!${var}` | 取反 |
|
|
312
|
+
| `${var} == 'yes'` | 字符串/数值相等 |
|
|
313
|
+
| `${var} != 'no'` | 不等 |
|
|
314
|
+
| `${var} > 5` | 数值比较(>=, <=, < 同理) |
|
|
315
|
+
| `${var}.includes('login')` | 包含 |
|
|
316
|
+
| `${var}.startsWith('https')` / `endsWith('.xlsx')` | 前/后缀 |
|
|
317
|
+
| `!${var}.includes('admin')` | 取反组合 |
|
|
318
|
+
|
|
319
|
+
**故意不支持** `&&` / `||` / `()`。要复合逻辑用多步 + `if` + `on_fail` 拆开。
|
|
320
|
+
|
|
321
|
+
### 失败时的客户体验
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
$ uniner-cli skill run tms-export --param URL=...
|
|
325
|
+
{"success": false, "failed_step": "click-search", ...}
|
|
326
|
+
|
|
327
|
+
────────── How to debug ──────────
|
|
328
|
+
Step 'click-search' failed: timeout
|
|
329
|
+
Open Claude Code, paste this:
|
|
330
|
+
|
|
331
|
+
I ran 'uniner-cli skill run' and step 'click-search' failed.
|
|
332
|
+
Error: timeout
|
|
333
|
+
Please look at the scenario at:
|
|
334
|
+
C:\Users\xxx\.claude\skills\uniner-cli\scenarios\tms-export
|
|
335
|
+
Especially steps.json. Help me figure out why and suggest a fix.
|
|
336
|
+
──────────────────────────────────
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
客户复制这段给你 → 你 Read steps.json 调试 → 改完 `Write` 回去 → 让客户重跑。
|
|
340
|
+
|
|
341
|
+
### 给 Claude 的对话范式(用户同意保存后)
|
|
342
|
+
|
|
343
|
+
按这个顺序,**不要跳步**:
|
|
344
|
+
|
|
345
|
+
1. 想一个 kebab-case 名字(如 `tms-export-waybills` / `sap-monthly-report`)
|
|
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>` 确认格式无误
|
|
352
|
+
5. 让用户 `uniner-cli skill run <name> --param ...` 自己跑一次确认 0 token 重放可行
|
|
353
|
+
|
|
354
|
+
**关键**:steps.json 不是命令清单的注释——是**真正可执行的工作流**。要把 `${URL}` `${USERNAME}` `${ocrResult.data.result}` 这种变量绑好,让 `skill run` 真能跑通。
|
|
355
|
+
|
|
356
|
+
### 已沉淀的场景库
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
uniner-cli skill list # 看所有
|
|
360
|
+
uniner-cli skill show <name> # 看某个的元数据 + 步骤摘要
|
|
361
|
+
uniner-cli skill show <name> --steps # 看完整步骤
|
|
362
|
+
uniner-cli skill where # 看场景库根目录(可手动浏览)
|
|
363
|
+
uniner-cli skill delete <name> --yes # 删除(默认 dry-run)
|
|
364
|
+
```
|
|
365
|
+
|
|
182
366
|
## 不确定时
|
|
183
367
|
|
|
184
368
|
- `uniner-cli --help` 看一级
|
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
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.2",
|
|
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
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -231,8 +231,28 @@ function writeSkillMd(installPath, version) {
|
|
|
231
231
|
|
|
232
232
|
const dir = skillDir();
|
|
233
233
|
fs.mkdirSync(dir, { recursive: true });
|
|
234
|
+
|
|
235
|
+
// IMPORTANT: only writeFileSync the single SKILL.md file. Do NOT touch
|
|
236
|
+
// anything else under ~/.claude/skills/uniner-cli/. In particular, the
|
|
237
|
+
// scenarios/ subdirectory is the user's accumulated knowledge — if we
|
|
238
|
+
// ever rmdir or rmSync this directory we'd wipe their library on every
|
|
239
|
+
// upgrade. Single-file write is the contract.
|
|
234
240
|
fs.writeFileSync(skillFile(), content, 'utf8');
|
|
235
241
|
log(` SKILL.md written to ${skillFile()}`);
|
|
242
|
+
|
|
243
|
+
// Tell the user we kept their scenarios (if any).
|
|
244
|
+
const scenariosDir = path.join(dir, 'scenarios');
|
|
245
|
+
if (fs.existsSync(scenariosDir)) {
|
|
246
|
+
try {
|
|
247
|
+
const count = fs.readdirSync(scenariosDir).filter(n => {
|
|
248
|
+
const meta = path.join(scenariosDir, n, 'meta.json');
|
|
249
|
+
return fs.existsSync(meta);
|
|
250
|
+
}).length;
|
|
251
|
+
log(` [INFO] Preserved ${count} scenario(s) in ${scenariosDir}`);
|
|
252
|
+
} catch (_) {
|
|
253
|
+
// best-effort; never fail postinstall over a logging stat
|
|
254
|
+
}
|
|
255
|
+
}
|
|
236
256
|
}
|
|
237
257
|
|
|
238
258
|
// ----------------------------------------------------------------------
|