uniner-cli 1.0.0 → 1.0.1
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 +171 -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
|
@@ -179,6 +179,177 @@ uniner-cli excel data write-range --file "{{INSTALL_PATH}}\workdir\out.xlsx" --c
|
|
|
179
179
|
uniner-cli web browser close
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
## 场景沉淀(重要:完成任务后主动建议保存)
|
|
183
|
+
|
|
184
|
+
每完成一个完整 RPA 流程后,**主动建议**用户把它沉淀为可重放的场景。
|
|
185
|
+
之后用户用 `uniner-cli skill run <name>` 即可 **0 token 重放**——
|
|
186
|
+
不再需要 Claude,直接命令行就能跑、能放进定时任务 / Airflow / RPA 平台。
|
|
187
|
+
|
|
188
|
+
### 何时建议保存
|
|
189
|
+
|
|
190
|
+
下面任一情况都该建议:
|
|
191
|
+
- 任务跑完且至少调用了 ≥3 个 uniner-cli 命令
|
|
192
|
+
- 任务里踩过坑(XPath 不稳、参数名错、Element UI 组件等)
|
|
193
|
+
- 用户表达过 "以后还会跑"
|
|
194
|
+
|
|
195
|
+
### 怎么保存:3 个文件 + 4 步流程
|
|
196
|
+
|
|
197
|
+
每个场景一个目录,三件套:`meta.json` + `steps.json` + `SKILL.md`,存到:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
%USERPROFILE%\.claude\skills\uniner-cli\scenarios\<kebab-case-name>\
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**4 步标准流程**:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Step 1: 生成骨架(自动创建目录和模板文件)
|
|
207
|
+
uniner-cli skill init <name>
|
|
208
|
+
|
|
209
|
+
# Step 2: 用 Write 工具改 meta.json / steps.json / SKILL.md(见下方 schema)
|
|
210
|
+
|
|
211
|
+
# Step 3: 校验
|
|
212
|
+
uniner-cli skill validate <name>
|
|
213
|
+
|
|
214
|
+
# Step 4: 试跑一次
|
|
215
|
+
uniner-cli skill run <name> --param ...
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### meta.json schema
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"name": "<kebab-case-name>",
|
|
223
|
+
"version": "1.0",
|
|
224
|
+
"description": "一句话描述(会被用来匹配触发关键词)",
|
|
225
|
+
"params": {
|
|
226
|
+
"URL": { "type": "string", "required": true, "description": "登录页地址" },
|
|
227
|
+
"USERNAME": { "type": "string", "required": true, "description": "账号" },
|
|
228
|
+
"PASSWORD": { "type": "string", "required": true, "sensitive": true, "description": "密码" },
|
|
229
|
+
"DATE_FROM": { "type": "string", "required": false, "default": "today", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### steps.json schema
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"version": "1.0",
|
|
239
|
+
"name": "<同 meta.name>",
|
|
240
|
+
"steps": [
|
|
241
|
+
{
|
|
242
|
+
"id": "open-browser",
|
|
243
|
+
"cmd": "web browser open",
|
|
244
|
+
"args": { "url": "${URL}" },
|
|
245
|
+
"timeout_ms": 10000
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"id": "ocr-captcha",
|
|
249
|
+
"cmd": "tool ocr ddddocr",
|
|
250
|
+
"args": { "img": "workdir/captcha.png" },
|
|
251
|
+
"store": "ocrResult",
|
|
252
|
+
"comment": "整个 stdout JSON 存到 ocrResult,下面用 ${ocrResult.data.result} 取"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"id": "input-captcha",
|
|
256
|
+
"cmd": "web element input",
|
|
257
|
+
"args": { "xpath": "//input[@placeholder='验证码']", "value": "${ocrResult.data.result}" }
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"id": "close-browser",
|
|
261
|
+
"cmd": "web browser close",
|
|
262
|
+
"always_run": true,
|
|
263
|
+
"comment": "无论前面成功与否都跑(清理用)"
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 每个 step 支持的字段
|
|
270
|
+
|
|
271
|
+
| 字段 | 必需 | 说明 |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `id` | ✅ | 步骤 ID(唯一) |
|
|
274
|
+
| `cmd` | ✅ | uniner-cli 子命令,**不带 `uniner-cli` 前缀**,如 `web element input` |
|
|
275
|
+
| `args` | ✅ | dict 形式参数,对应 CLI 的 `--key value` |
|
|
276
|
+
| `store` | ❌ | 把 stdout JSON 整个存到变量名 |
|
|
277
|
+
| `if` | ❌ | 条件表达式(见下) |
|
|
278
|
+
| `on_fail` | ❌ | `abort`(默认)/ `continue` / `goto:<step_id>` |
|
|
279
|
+
| `retry` | ❌ | 失败重试次数(不含首次) |
|
|
280
|
+
| `retry_delay_ms` | ❌ | 重试间隔(默认 1000) |
|
|
281
|
+
| `timeout_ms` | ❌ | 单步超时(覆盖全局) |
|
|
282
|
+
| `always_run` | ❌ | 即使前面失败也跑(清理用) |
|
|
283
|
+
| `comment` | ❌ | 给人/AI 看的注释 |
|
|
284
|
+
|
|
285
|
+
### 变量替换语法
|
|
286
|
+
|
|
287
|
+
| 写法 | 含义 |
|
|
288
|
+
|---|---|
|
|
289
|
+
| `${URL}` | 用户传入的 `--param URL=xxx` |
|
|
290
|
+
| `${ocrResult.data.result}` | 上一步 store 的 dict 字段(点号路径) |
|
|
291
|
+
| `${tableData.0.1}` | list 索引(0-based) |
|
|
292
|
+
| `${env:USERPROFILE}` | 环境变量 |
|
|
293
|
+
|
|
294
|
+
### 条件表达式语法(白名单)
|
|
295
|
+
|
|
296
|
+
| 写法 | 含义 |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `${var}` | 真值判断 |
|
|
299
|
+
| `!${var}` | 取反 |
|
|
300
|
+
| `${var} == 'yes'` | 字符串/数值相等 |
|
|
301
|
+
| `${var} != 'no'` | 不等 |
|
|
302
|
+
| `${var} > 5` | 数值比较(>=, <=, < 同理) |
|
|
303
|
+
| `${var}.includes('login')` | 包含 |
|
|
304
|
+
| `${var}.startsWith('https')` / `endsWith('.xlsx')` | 前/后缀 |
|
|
305
|
+
| `!${var}.includes('admin')` | 取反组合 |
|
|
306
|
+
|
|
307
|
+
**故意不支持** `&&` / `||` / `()`。要复合逻辑用多步 + `if` + `on_fail` 拆开。
|
|
308
|
+
|
|
309
|
+
### 失败时的客户体验
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
$ uniner-cli skill run tms-export --param URL=...
|
|
313
|
+
{"success": false, "failed_step": "click-search", ...}
|
|
314
|
+
|
|
315
|
+
────────── How to debug ──────────
|
|
316
|
+
Step 'click-search' failed: timeout
|
|
317
|
+
Open Claude Code, paste this:
|
|
318
|
+
|
|
319
|
+
I ran 'uniner-cli skill run' and step 'click-search' failed.
|
|
320
|
+
Error: timeout
|
|
321
|
+
Please look at the scenario at:
|
|
322
|
+
C:\Users\xxx\.claude\skills\uniner-cli\scenarios\tms-export
|
|
323
|
+
Especially steps.json. Help me figure out why and suggest a fix.
|
|
324
|
+
──────────────────────────────────
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
客户复制这段给你 → 你 Read steps.json 调试 → 改完 `Write` 回去 → 让客户重跑。
|
|
328
|
+
|
|
329
|
+
### 给 Claude 的对话范式
|
|
330
|
+
|
|
331
|
+
完成任务后说类似的话(**主动询问,不等用户开口**):
|
|
332
|
+
|
|
333
|
+
> "这个流程涉及 OCR、Vue 组件、XPath 等不稳定环节,我帮你保存为可重放场景吧?以后你直接 `uniner-cli skill run <name>` 就能 0 token 重放,还能放定时任务里跑。要保存吗?"
|
|
334
|
+
|
|
335
|
+
得到确认后:
|
|
336
|
+
|
|
337
|
+
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>`
|
|
341
|
+
5. 让用户 `uniner-cli skill run <name> --param ...` 自己跑一次确认 0 token 重放可行
|
|
342
|
+
|
|
343
|
+
### 已沉淀的场景库
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
uniner-cli skill list # 看所有
|
|
347
|
+
uniner-cli skill show <name> # 看某个的元数据 + 步骤摘要
|
|
348
|
+
uniner-cli skill show <name> --steps # 看完整步骤
|
|
349
|
+
uniner-cli skill where # 看场景库根目录(可手动浏览)
|
|
350
|
+
uniner-cli skill delete <name> --yes # 删除(默认 dry-run)
|
|
351
|
+
```
|
|
352
|
+
|
|
182
353
|
## 不确定时
|
|
183
354
|
|
|
184
355
|
- `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.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
|
+
}
|
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
|
// ----------------------------------------------------------------------
|