@blod-nm/1kuan-act-01-cli 0.1.3 → 0.1.5
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 +14 -2
- package/package.json +2 -3
- package/src/main.js +98 -7
- package/src/skill.js +20 -4
- package/src/version.js +1 -1
- package/config-helper.html +0 -220
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ act-01 config test
|
|
|
22
22
|
|
|
23
23
|
Stored at `~/.config/act-01/config.json` (0600, atomic write). `apiKey` can also come from the `ACT01_API_KEY` environment variable. Never pass it as a CLI argument.
|
|
24
24
|
|
|
25
|
-
No local config?
|
|
25
|
+
No local config? Ask the team for a `config-helper.html` (a single local HTML page) to generate a transfer string, then:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
act-01 config import "act01w.v1.xxxx" # prompts for the passphrase used at generation
|
|
@@ -30,15 +30,27 @@ act-01 config import "act01w.v1.xxxx" # prompts for the passphrase used at gen
|
|
|
30
30
|
|
|
31
31
|
Transfer strings are AES-256-GCM encrypted; the passphrase never travels with the string.
|
|
32
32
|
|
|
33
|
+
## One-shot setup
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
ACT01_EXPORT_PASS="<passphrase>" act-01 setup --install ~/.claude/skills "<config-string>"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Installs the skill doc, imports the config, and prints the balance in one command
|
|
40
|
+
(passphrase via env; config string also accepted on stdin or `ACT01_IMPORT_STRING`).
|
|
41
|
+
`--install <dir>` writes `<dir>/act-01-ops/SKILL.md`.
|
|
42
|
+
|
|
33
43
|
## Operations
|
|
34
44
|
|
|
35
45
|
```bash
|
|
36
|
-
act-01
|
|
46
|
+
act-01 redpacket check --mode random --total 10000 --count 50 --per-user 2 --min 100 --max 500
|
|
47
|
+
act-01 redpacket create --mode random --total 10000 --count 50 --per-user 2 --min 100 --max 500 --yes
|
|
37
48
|
```
|
|
38
49
|
|
|
39
50
|
- Amounts are integer units, no conversion
|
|
40
51
|
- Pre-flight balance check runs before execution; insufficient balance is rejected locally
|
|
41
52
|
- Interactive confirm card lists every parameter before the atomic database function call
|
|
53
|
+
- `create` is non-interactive only with `--yes` (otherwise it asks for confirmation)
|
|
42
54
|
- `--dry-run` prints the function call without executing; `--json` for machine output
|
|
43
55
|
|
|
44
56
|
## MCP (AI client integration)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blod-nm/1kuan-act-01-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Internal operations CLI: invoke preset database functions via a gateway. Zero dependencies, MCP (stdio) support.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"bin",
|
|
16
16
|
"src",
|
|
17
|
-
"config-helper.html",
|
|
18
17
|
"README.md"
|
|
19
18
|
],
|
|
20
19
|
"scripts": {
|
|
@@ -26,7 +25,7 @@
|
|
|
26
25
|
"build:all": "bun run build:js && bun run build:bin:all"
|
|
27
26
|
},
|
|
28
27
|
"engines": {
|
|
29
|
-
"node": ">=18"
|
|
28
|
+
"node": ">=18.3"
|
|
30
29
|
},
|
|
31
30
|
"keywords": [
|
|
32
31
|
"cli",
|
package/src/main.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { parseArgs } from 'node:util'
|
|
6
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
6
7
|
import { VERSION } from './version.js'
|
|
7
8
|
import { join } from 'node:path'
|
|
8
9
|
import { configPath, expandPath, loadConfig, saveConfig, decodeConfigString, encryptConfig } from './store.js'
|
|
@@ -23,8 +24,9 @@ const USAGE = `act-01 —— operations CLI(invoke preset database functions)
|
|
|
23
24
|
config test 连接测试(SELECT 1,验证 envId/apiKey)
|
|
24
25
|
config export 导出加密配置串(口令交互输入或 ACT01_EXPORT_PASS)
|
|
25
26
|
config import <串|--stdin> [--pass <口令>] 导入配置串(加密串需口令;自动识别 三种格式)
|
|
27
|
+
setup <串|--stdin> [--install <技能根目录>] 一条命令:装技能 + 导配置 + 查余额
|
|
26
28
|
mcp 启动 MCP 服务(stdio;接入 Claude 等 AI 客户端)
|
|
27
|
-
skill 输出 agent 技能文档(--install <
|
|
29
|
+
skill 输出 agent 技能文档(--install <技能根目录> 装成 <root>/act-01-ops/SKILL.md)
|
|
28
30
|
balance account balance
|
|
29
31
|
redpacket create create a record(pre-flight balance check, atomic deduct)
|
|
30
32
|
redpacket check validate params(side-effect free)
|
|
@@ -150,6 +152,23 @@ async function cmdConfig(argv) {
|
|
|
150
152
|
throw err
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
if (sub === 'test') {
|
|
156
|
+
const config = loadConfig()
|
|
157
|
+
const apikey = resolveApikey(config, undefined)
|
|
158
|
+
if (!config.envId || !apikey) {
|
|
159
|
+
const err = new Error('配置不完整:先 config set envId 和 apiKey')
|
|
160
|
+
err.exitCode = 2
|
|
161
|
+
throw err
|
|
162
|
+
}
|
|
163
|
+
const r = await execPgsql(config, apikey, 'SELECT 1 AS ok')
|
|
164
|
+
if (!r.ok) {
|
|
165
|
+
console.error(`error: 连接测试失败 (${r.message})`)
|
|
166
|
+
return 1
|
|
167
|
+
}
|
|
168
|
+
console.log('连接测试通过(SELECT 1)')
|
|
169
|
+
return 0
|
|
170
|
+
}
|
|
171
|
+
|
|
153
172
|
if (sub === 'export') {
|
|
154
173
|
const config = loadConfig()
|
|
155
174
|
if (!config.envId || !config.apiKey) {
|
|
@@ -317,6 +336,15 @@ function normalizeFromValues(values) {
|
|
|
317
336
|
|
|
318
337
|
/* ---------- skill ---------- */
|
|
319
338
|
|
|
339
|
+
/** 把 SKILL.md 装到 <root>/<SKILL_NAME>/SKILL.md(目录式,匹配主流 agent 技能布局) */
|
|
340
|
+
function installSkill(root) {
|
|
341
|
+
const dir = join(expandPath(root), SKILL_NAME)
|
|
342
|
+
mkdirSync(dir, { recursive: true, mode: 0o755 })
|
|
343
|
+
const file = join(dir, 'SKILL.md')
|
|
344
|
+
writeFileSync(file, SKILL_MD)
|
|
345
|
+
return file
|
|
346
|
+
}
|
|
347
|
+
|
|
320
348
|
async function cmdSkill(argv) {
|
|
321
349
|
const { values, positionals } = parseArgs({
|
|
322
350
|
strict: false,
|
|
@@ -332,13 +360,9 @@ async function cmdSkill(argv) {
|
|
|
332
360
|
}
|
|
333
361
|
|
|
334
362
|
if (values.install) {
|
|
335
|
-
const
|
|
336
|
-
const dir = expandPath(values.install)
|
|
337
|
-
mkdirSync(dir, { recursive: true, mode: 0o755 })
|
|
338
|
-
const file = join(dir, 'SKILL.md')
|
|
339
|
-
writeFileSync(file, SKILL_MD)
|
|
363
|
+
const file = installSkill(values.install)
|
|
340
364
|
console.log(`SKILL.md 已写入: ${file}`)
|
|
341
|
-
console.error(
|
|
365
|
+
console.error(`hint: 已装成 <技能根目录>/${SKILL_NAME}/SKILL.md;根目录指你的 agent 扫描的那个 skills 目录`)
|
|
342
366
|
return 0
|
|
343
367
|
}
|
|
344
368
|
|
|
@@ -346,6 +370,72 @@ async function cmdSkill(argv) {
|
|
|
346
370
|
return 0
|
|
347
371
|
}
|
|
348
372
|
|
|
373
|
+
/* ---------- setup(一条命令:装技能 + 导配置 + 查余额) ---------- */
|
|
374
|
+
|
|
375
|
+
/** 解析配置串(明文/加密)、写盘;返回 { envId, apiKey } */
|
|
376
|
+
function importConfig(body, passFlag) {
|
|
377
|
+
const decoded = decodeConfigString(body)
|
|
378
|
+
let cfg = decoded.config
|
|
379
|
+
if (decoded.encrypted) {
|
|
380
|
+
const pass = passFlag ?? process.env.ACT01_EXPORT_PASS
|
|
381
|
+
if (!pass) {
|
|
382
|
+
const err = new Error('加密串需要口令:用 ACT01_EXPORT_PASS 环境变量或 --pass 传入')
|
|
383
|
+
err.exitCode = 2
|
|
384
|
+
throw err
|
|
385
|
+
}
|
|
386
|
+
cfg = decoded.decode(pass)
|
|
387
|
+
}
|
|
388
|
+
if (!cfg.envId || !cfg.apiKey) {
|
|
389
|
+
const err = new Error('配置串内容不完整(缺 envId 或 apiKey)')
|
|
390
|
+
err.exitCode = 2
|
|
391
|
+
throw err
|
|
392
|
+
}
|
|
393
|
+
saveConfig({ envId: cfg.envId, apiKey: cfg.apiKey })
|
|
394
|
+
return cfg
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function cmdSetup(argv) {
|
|
398
|
+
const { values, positionals } = parseArgs({
|
|
399
|
+
strict: false,
|
|
400
|
+
allowPositionals: true,
|
|
401
|
+
args: argv,
|
|
402
|
+
options: { install: { type: 'string' }, pass: { type: 'string' }, json: { type: 'boolean', short: 'j' } },
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
let body = positionals[0] ?? process.env.ACT01_IMPORT_STRING
|
|
406
|
+
if (!body && !process.stdin.isTTY) body = await readStdinText()
|
|
407
|
+
if (!body) {
|
|
408
|
+
const err = new Error('setup 需要 <配置串>(位置参数 / stdin / ACT01_IMPORT_STRING)')
|
|
409
|
+
err.exitCode = 2
|
|
410
|
+
throw err
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const cfg = importConfig(body, str(values, 'pass'))
|
|
414
|
+
const skillPath = values.install ? installSkill(values.install) : null
|
|
415
|
+
|
|
416
|
+
const account = await execPgsql(cfg, resolveApikey(cfg, undefined), ACCOUNT_SQL)
|
|
417
|
+
const wallet =
|
|
418
|
+
account.ok && account.rows?.[0]?.data?.wallet && typeof account.rows[0].data.wallet === 'object'
|
|
419
|
+
? account.rows[0].data.wallet
|
|
420
|
+
: {}
|
|
421
|
+
const points = account.ok ? Number(wallet.points || 0) : null
|
|
422
|
+
const label = account.ok ? String(wallet.label || 'account') : null
|
|
423
|
+
|
|
424
|
+
if (values.json === true) {
|
|
425
|
+
printJson({ ok: account.ok, envId: cfg.envId, apiKeySet: true, skillPath, balance: points, account: label, error: account.ok ? null : account.message })
|
|
426
|
+
} else {
|
|
427
|
+
console.log('setup 完成:')
|
|
428
|
+
console.log(skillPath ? ` 技能: SKILL.md → ${skillPath}` : ' 技能: (未装;加 --install <技能根目录>)')
|
|
429
|
+
console.log(` 配置: envId ${cfg.envId},apiKey 已打码写入 0600`)
|
|
430
|
+
if (account.ok) console.log(` 余额: ${points}(${label})`)
|
|
431
|
+
else {
|
|
432
|
+
console.error(` 余额: 查询失败 (${account.message})`)
|
|
433
|
+
return 1
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return 0
|
|
437
|
+
}
|
|
438
|
+
|
|
349
439
|
/* ---------- redpacket check(零副作用参数校验) ---------- */
|
|
350
440
|
|
|
351
441
|
async function cmdCheck(argv) {
|
|
@@ -527,6 +617,7 @@ export async function main(argv) {
|
|
|
527
617
|
if (cmd === 'balance') return await cmdBalance(argv.slice(1))
|
|
528
618
|
if (cmd === 'mcp') return await startMcp()
|
|
529
619
|
if (cmd === 'skill') return await cmdSkill(argv.slice(1))
|
|
620
|
+
if (cmd === 'setup') return await cmdSetup(argv.slice(1))
|
|
530
621
|
if (cmd === 'redpacket') {
|
|
531
622
|
const sub = argv[1]
|
|
532
623
|
if (sub === 'create') return await cmdCreate(argv.slice(2))
|
package/src/skill.js
CHANGED
|
@@ -23,6 +23,11 @@ const BODY = [
|
|
|
23
23
|
'',
|
|
24
24
|
'所有命令零依赖,Node >= 18。',
|
|
25
25
|
'',
|
|
26
|
+
'## 一条命令装好(推荐)',
|
|
27
|
+
'',
|
|
28
|
+
B + 'act-01 setup "<配置串>" --install <技能根目录>' + B + ' —— 一次完成:装技能 + 导配置 + 查余额,',
|
|
29
|
+
'口令走 ' + B + 'ACT01_EXPORT_PASS' + B + ' 环境变量;配置串也可走 stdin 或 ' + B + 'ACT01_IMPORT_STRING' + B + '。',
|
|
30
|
+
'',
|
|
26
31
|
'## 工具面',
|
|
27
32
|
'',
|
|
28
33
|
'| 途径 | 命令 / MCP 工具 | 副作用 |',
|
|
@@ -51,10 +56,21 @@ const BODY = [
|
|
|
51
56
|
'2. **确认卡**:执行前列出全部参数,等用户确认',
|
|
52
57
|
'3. **库函数原子扣减**:并发/余额竞争由数据库兜底,不会扣穿',
|
|
53
58
|
'',
|
|
59
|
+
'## 日常最快路径(一句大白话 → 一次确认 → 执行)',
|
|
60
|
+
'',
|
|
61
|
+
'1. ' + B + 'act-01 redpacket check <flags>' + B + ' 拿校验结果(零副作用)',
|
|
62
|
+
'2. 把校验结果逐项复述给用户,等确认',
|
|
63
|
+
'3. 确认后执行:' + B + 'act-01 redpacket create <flags> --yes' + B + '(非交互必须带 ' + B + '--yes' + B + ')',
|
|
64
|
+
'',
|
|
65
|
+
'余额为 0 属正常:create 会被预检拦下,先让用户到运营台充值。',
|
|
66
|
+
'',
|
|
67
|
+
'> 更轻的日常形态(可选):' + B + 'act-01 mcp' + B + ' 接入支持 MCP 的客户端后,参数规格在工具 schema 里,',
|
|
68
|
+
'> 不必读本文档;客户端审批弹窗即"确认卡"。',
|
|
69
|
+
'',
|
|
54
70
|
'## Agent 行为规则',
|
|
55
71
|
'',
|
|
56
72
|
'1. **先 check 后 create**:参数没把握时,先调 redpacket_check,拿到校验报告再执行',
|
|
57
|
-
'2. **create 前必须向用户确认参数**(mode/total/count/区间/群归属)
|
|
73
|
+
'2. **create 前必须向用户确认参数**(mode/total/count/区间/群归属);确认后执行时,**非交互(非 TTY)必须带 --yes**,否则 CLI 会拒绝(MCP 客户端则弹审批)',
|
|
58
74
|
'3. **结果转述只给摘要**:packet_no、余额变化;不要猜测或编造未返回的字段',
|
|
59
75
|
'4. **失败处理**:错误信息是人话(余额不足/区间错误/整除错误),原样转告用户并给出修正建议;不要重试同参数的 create',
|
|
60
76
|
'5. **金额单位**:内部整数单位,不要做 ×100 或 ÷100 的换算,除非用户明确要求',
|
|
@@ -73,10 +89,10 @@ const BODY = [
|
|
|
73
89
|
'',
|
|
74
90
|
'## 配置前置',
|
|
75
91
|
'',
|
|
76
|
-
'目标机器需要云开发配置(envId + apiKey)
|
|
77
|
-
'(
|
|
92
|
+
'目标机器需要云开发配置(envId + apiKey)。没有配置时,让用户给你一条加密配置串',
|
|
93
|
+
'(团队用生成页产出),一条命令装好:',
|
|
78
94
|
'',
|
|
79
|
-
B + 'act-01
|
|
95
|
+
B + 'ACT01_EXPORT_PASS="<口令>" act-01 setup --install <技能根目录> "<配置串>"' + B,
|
|
80
96
|
'',
|
|
81
97
|
].join('\n')
|
|
82
98
|
|
package/src/version.js
CHANGED
package/config-helper.html
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>act-01 配置串生成器</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root { color-scheme: light dark; }
|
|
9
|
-
* { box-sizing: border-box; }
|
|
10
|
-
body {
|
|
11
|
-
margin: 0; padding: 24px;
|
|
12
|
-
font: 15px/1.65 -apple-system, 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
|
|
13
|
-
background: light-dark(#f6f4ef, #17151f); color: light-dark(#262233, #e8e5f0);
|
|
14
|
-
font-variant-numeric: tabular-nums; -webkit-font-smoothing: antialiased;
|
|
15
|
-
}
|
|
16
|
-
.wrap { max-width: 640px; margin: 0 auto; display: grid; gap: 20px; }
|
|
17
|
-
h1 { font-size: 20px; margin: 0 0 4px; letter-spacing: -0.01em; }
|
|
18
|
-
.sub { color: light-dark(#7a7490, #9a94b0); font-size: 13px; margin: 0; }
|
|
19
|
-
.card {
|
|
20
|
-
border: 2px solid light-dark(#413a5e, #cbc5e0); border-radius: 16px;
|
|
21
|
-
padding: 20px; background: light-dark(#fffdf8, #201d2b);
|
|
22
|
-
box-shadow: 5px 5px 0 light-dark(#413a5e, #0c0a12);
|
|
23
|
-
}
|
|
24
|
-
.card h2 { font-size: 15px; margin: 0 0 12px; }
|
|
25
|
-
label { display: block; font-size: 13px; font-weight: 600; margin: 0 0 4px; }
|
|
26
|
-
label .opt { font-weight: 400; opacity: .65; }
|
|
27
|
-
input[type=text], input[type=password] {
|
|
28
|
-
width: 100%; padding: 9px 12px; font: inherit; font-size: 14px;
|
|
29
|
-
border: 2px solid light-dark(#413a5e, #cbc5e0); border-radius: 10px;
|
|
30
|
-
background: light-dark(#fff, #262233); color: inherit; margin-bottom: 12px;
|
|
31
|
-
}
|
|
32
|
-
input:focus { outline: 2px solid light-dark(#7c5cff, #a78bfa); outline-offset: 1px; }
|
|
33
|
-
button {
|
|
34
|
-
padding: 9px 18px; font: inherit; font-size: 14px; font-weight: 600;
|
|
35
|
-
border: 2px solid light-dark(#413a5e, #cbc5e0); border-radius: 999px;
|
|
36
|
-
background: light-dark(#7c5cff, #a78bfa); color: light-dark(#fff, #1a1526);
|
|
37
|
-
box-shadow: 3px 3px 0 light-dark(#413a5e, #0c0a12); cursor: pointer;
|
|
38
|
-
transition: transform 120ms cubic-bezier(0.34,1.56,0.64,1), box-shadow 120ms;
|
|
39
|
-
}
|
|
40
|
-
button:active { transform: translate(2px,2px); box-shadow: 1px 1px 0 light-dark(#413a5e, #0c0a12); }
|
|
41
|
-
button.ghost { background: transparent; color: inherit; }
|
|
42
|
-
.out {
|
|
43
|
-
font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 12px;
|
|
44
|
-
word-break: break-all; padding: 10px 12px; border-radius: 10px;
|
|
45
|
-
background: light-dark(#fff, #262233);
|
|
46
|
-
border: 2px solid light-dark(#413a5e, #cbc5e0); min-height: 42px; white-space: pre-wrap;
|
|
47
|
-
}
|
|
48
|
-
.row { display: flex; gap: 10px; align-items: center; }
|
|
49
|
-
.row input { flex: 1; margin-bottom: 0; }
|
|
50
|
-
.hint { font-size: 12px; opacity: .65; margin: 6px 0 0; }
|
|
51
|
-
.err { color: light-dark(#c2334d, #ff8fa3); font-size: 13px; margin: 8px 0 0; }
|
|
52
|
-
.ok { color: light-dark(#0a7d55, #4ade80); font-size: 13px; margin: 8px 0 0; }
|
|
53
|
-
code {
|
|
54
|
-
font-family: ui-monospace, Menlo, monospace; font-size: 12px;
|
|
55
|
-
background: light-dark(#efece3, #262233); padding: 2px 6px; border-radius: 6px;
|
|
56
|
-
}
|
|
57
|
-
.toggle { display: flex; gap: 14px; font-size: 13px; margin-bottom: 12px; }
|
|
58
|
-
</style>
|
|
59
|
-
</head>
|
|
60
|
-
<body>
|
|
61
|
-
<div class="wrap">
|
|
62
|
-
<header>
|
|
63
|
-
<h1>🔑 act-01 配置串生成器</h1>
|
|
64
|
-
<p class="sub">填入云开发原始配置 → 生成加密传递串。全本地计算,无网络请求,无任何存储。</p>
|
|
65
|
-
</header>
|
|
66
|
-
|
|
67
|
-
<div class="card">
|
|
68
|
-
<h2>① 填入原始配置</h2>
|
|
69
|
-
<label>envId(环境 ID)</label>
|
|
70
|
-
<input id="envId" type="text" placeholder="cloud1-xxxxxx" autocomplete="off">
|
|
71
|
-
|
|
72
|
-
<label>apiKey(腾讯云开发 API Key,需管理员权限)</label>
|
|
73
|
-
<input id="apiKey" type="password" placeholder="act- 开头或平台管理生成" autocomplete="off">
|
|
74
|
-
|
|
75
|
-
<label>口令 <span class="opt">— 加密用,导入端必须输入同一个;不含在配置串里</span></label>
|
|
76
|
-
<div class="row">
|
|
77
|
-
<input id="pass" type="password" placeholder="自己定一个,别用弱口令" autocomplete="new-password">
|
|
78
|
-
<button class="ghost" id="genPass">随机生成</button>
|
|
79
|
-
</div>
|
|
80
|
-
|
|
81
|
-
<div class="toggle">
|
|
82
|
-
<label style="margin:0;font-weight:400"><input type="radio" name="mode" value="enc" checked> 加密串(需口令解密,推荐)</label>
|
|
83
|
-
<label style="margin:0;font-weight:400"><input type="radio" name="mode" value="plain"> 明文串(便捷,可读)</label>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<button id="gen">生成配置串</button>
|
|
87
|
-
<p class="err" id="err" hidden></p>
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
<div class="card" id="outCard" hidden>
|
|
91
|
-
<h2>② 配置串(复制发出去)</h2>
|
|
92
|
-
<div class="out" id="out"></div>
|
|
93
|
-
<div class="row" style="margin-top:10px">
|
|
94
|
-
<button id="copy">复制</button>
|
|
95
|
-
<span class="hint" style="align-self:center">目标机器执行:<code>act-01 config import --stdin < 串文件</code>(口令会提示输入)</span>
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
<div class="card">
|
|
100
|
-
<h2>③ 校验(可选)</h2>
|
|
101
|
-
<label>粘贴配置串</label>
|
|
102
|
-
<input id="vStr" type="text" autocomplete="off" class="mono-out">
|
|
103
|
-
<label>口令</label>
|
|
104
|
-
<input id="vPass" type="password" autocomplete="off">
|
|
105
|
-
<button class="ghost" id="verify">解密校验</button>
|
|
106
|
-
<p class="err" id="vErr" hidden></p>
|
|
107
|
-
<p class="ok" id="vOk" hidden></p>
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
<script>
|
|
112
|
-
const $ = (id) => document.getElementById(id)
|
|
113
|
-
const enc = new TextEncoder()
|
|
114
|
-
|
|
115
|
-
function b64url(bytes) {
|
|
116
|
-
let bin = ''
|
|
117
|
-
for (const b of bytes) bin += String.fromCharCode(b)
|
|
118
|
-
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function b64urlDecode(text) {
|
|
122
|
-
const bin = atob(text.replace(/-/g, '+').replace(/_/g, '/'))
|
|
123
|
-
return Uint8Array.from(bin, (c) => c.charCodeAt(0))
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function deriveKey(pass, salt) {
|
|
127
|
-
const km = await crypto.subtle.importKey('raw', enc.encode(pass), 'PBKDF2', false, ['deriveKey'])
|
|
128
|
-
return crypto.subtle.deriveKey(
|
|
129
|
-
{ name: 'PBKDF2', salt, iterations: 310_000, hash: 'SHA-256' },
|
|
130
|
-
km, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'],
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function encryptWeb(obj, pass) {
|
|
135
|
-
const salt = crypto.getRandomValues(new Uint8Array(16))
|
|
136
|
-
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
137
|
-
const key = await deriveKey(pass, salt)
|
|
138
|
-
const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(JSON.stringify(obj))))
|
|
139
|
-
const packed = new Uint8Array(salt.length + iv.length + ct.length)
|
|
140
|
-
packed.set(salt, 0); packed.set(iv, salt.length); packed.set(ct, salt.length + iv.length)
|
|
141
|
-
return 'act01w.v1.' + b64url(packed)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function decryptWeb(str, pass) {
|
|
145
|
-
const body = str.trim()
|
|
146
|
-
const data = b64urlDecode(body.slice(body.indexOf('.') + 1))
|
|
147
|
-
if (data.length < 44) throw new Error('配置串过短或已损坏')
|
|
148
|
-
const salt = data.slice(0, 16)
|
|
149
|
-
const iv = data.slice(16, 28)
|
|
150
|
-
const rest = data.slice(28)
|
|
151
|
-
const key = await deriveKey(pass, salt)
|
|
152
|
-
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, rest.slice(0, rest.length - 16))
|
|
153
|
-
return JSON.parse(new TextDecoder().decode(pt))
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function collect() {
|
|
157
|
-
const envId = $('envId').value.trim()
|
|
158
|
-
const apiKey = $('apiKey').value.trim()
|
|
159
|
-
const pass = $('pass').value
|
|
160
|
-
if (!envId) throw new Error('envId 未填写')
|
|
161
|
-
if (!apiKey) throw new Error('apiKey 未填写')
|
|
162
|
-
if ($('pass').value && pass.length < 8) throw new Error('口令至少 8 位')
|
|
163
|
-
return { config: { envId, apiKey }, pass }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function showErr(id, msg) {
|
|
167
|
-
const el = $(id); el.hidden = !msg; el.textContent = msg || ''
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
$('genPass').addEventListener('click', () => {
|
|
171
|
-
const bytes = crypto.getRandomValues(new Uint8Array(18))
|
|
172
|
-
let bin = ''; for (const b of bytes) bin += String.fromCharCode(b)
|
|
173
|
-
$('pass').value = btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
$('gen').addEventListener('click', async () => {
|
|
177
|
-
showErr('err', '')
|
|
178
|
-
$('outCard').hidden = true
|
|
179
|
-
try {
|
|
180
|
-
const { config, pass } = collect()
|
|
181
|
-
const mode = document.querySelector('input[name=mode]:checked').value
|
|
182
|
-
let out
|
|
183
|
-
if (mode === 'enc') {
|
|
184
|
-
if (!pass) throw new Error('加密模式需要口令')
|
|
185
|
-
out = await encryptWeb(config, pass)
|
|
186
|
-
} else {
|
|
187
|
-
out = 'act01plain.' + b64url(enc.encode(JSON.stringify(config)))
|
|
188
|
-
}
|
|
189
|
-
$('out').textContent = out
|
|
190
|
-
$('outCard').hidden = false
|
|
191
|
-
} catch (e) {
|
|
192
|
-
showErr('err', e.message)
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
$('copy').addEventListener('click', async () => {
|
|
197
|
-
await navigator.clipboard.writeText($('out').textContent)
|
|
198
|
-
$('copy').textContent = '已复制 ✓'
|
|
199
|
-
setTimeout(() => ($('copy').textContent = '复制'), 1200)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
$('verify').addEventListener('click', async () => {
|
|
203
|
-
showErr('vErr', ''); $('vOk').hidden = true
|
|
204
|
-
try {
|
|
205
|
-
const str = $('vStr').value.trim()
|
|
206
|
-
const pass = $('vPass').value
|
|
207
|
-
let cfg
|
|
208
|
-
if (str.startsWith('act01w.v1.')) cfg = await decryptWeb(str, pass)
|
|
209
|
-
else if (str.startsWith('act01.v1.')) throw new Error('这是 CLI scrypt 格式串,请用 act-01 CLI 解')
|
|
210
|
-
else if (str.startsWith('act01plain.')) cfg = JSON.parse(atob(str.slice('act01plain.'.length).replace(/-/g,'+').replace(/_/g,'/')))
|
|
211
|
-
else throw new Error('无法识别的串格式')
|
|
212
|
-
$('vOk').hidden = false
|
|
213
|
-
$('vOk').textContent = `✓ 解密成功:envId=${cfg.envId},apiKey=${cfg.apiKey.slice(0, 6)}…(不回显全文)`
|
|
214
|
-
} catch (e) {
|
|
215
|
-
showErr('vErr', e.message)
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
</script>
|
|
219
|
-
</body>
|
|
220
|
-
</html>
|