@blod-nm/1kuan-act-01-cli 0.1.4 → 0.2.0
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 +6 -1
- package/package.json +2 -3
- package/src/main.js +87 -11
- package/src/mcp.js +18 -2
- package/src/qr.js +449 -0
- package/src/redpacket.js +5 -0
- package/src/skill.js +16 -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
|
|
@@ -52,6 +52,11 @@ act-01 redpacket create --mode random --total 10000 --count 50 --per-user 2 --mi
|
|
|
52
52
|
- Interactive confirm card lists every parameter before the atomic database function call
|
|
53
53
|
- `create` is non-interactive only with `--yes` (otherwise it asks for confirmation)
|
|
54
54
|
- `--dry-run` prints the function call without executing; `--json` for machine output
|
|
55
|
+
- After a successful create the CLI renders a scannable QR on **stderr** (`--qr ansi|off`, default `ansi`);
|
|
56
|
+
`--qr-out <file.svg>` writes it to a file. The encoded content is
|
|
57
|
+
`mp://pages/act-01/open/index?packet_no=<no>`, so scanning it inside the wx mini program
|
|
58
|
+
(我的 → 常用 → 扫码支付) opens the claim page directly — no domain or WeChat link rule needed
|
|
59
|
+
- `--json` includes `packetNo` and `qr: { content, format, path }`
|
|
55
60
|
|
|
56
61
|
## MCP (AI client integration)
|
|
57
62
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blod-nm/1kuan-act-01-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
@@ -8,7 +8,8 @@ import { VERSION } from './version.js'
|
|
|
8
8
|
import { join } from 'node:path'
|
|
9
9
|
import { configPath, expandPath, loadConfig, saveConfig, decodeConfigString, encryptConfig } from './store.js'
|
|
10
10
|
import { ACCOUNT_SQL, execPgsql, resolveApikey, gatewayBase } from './tcb.js'
|
|
11
|
-
import { buildCreateSql, confirmSummary, normalizeCreateRedpacket } from './redpacket.js'
|
|
11
|
+
import { buildCreateSql, claimTarget, confirmSummary, normalizeCreateRedpacket } from './redpacket.js'
|
|
12
|
+
import { encode, renderAnsi, renderSvg } from './qr.js'
|
|
12
13
|
import { SKILL_MD, SKILL_NAME } from './skill.js'
|
|
13
14
|
import { startMcp } from './mcp.js'
|
|
14
15
|
|
|
@@ -39,6 +40,8 @@ const USAGE = `act-01 —— operations CLI(invoke preset database functions)
|
|
|
39
40
|
--require-group 群专属
|
|
40
41
|
--dry-run 只打印库函数调用,不执行
|
|
41
42
|
--yes 跳过确认
|
|
43
|
+
--qr ansi|svg|off 创建成功后输出二维码(ansi 打到 stderr;svg 需配 --qr-out)
|
|
44
|
+
--qr-out <文件> 二维码写入文件(.svg 矢量 / 其它字符画)
|
|
42
45
|
|
|
43
46
|
apiKey 等同数据库管理员:支持 ACT01_API_KEY 环境变量,永远不要放在命令行参数里。
|
|
44
47
|
|
|
@@ -67,6 +70,16 @@ async function readStdinText() {
|
|
|
67
70
|
return Buffer.concat(chunks).toString('utf8').trim()
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
/** 网关可能把 jsonb 当字符串回,统一解析一次 */
|
|
74
|
+
function parseJsonResult(value) {
|
|
75
|
+
if (typeof value !== 'string') return value
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(value)
|
|
78
|
+
} catch {
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
function printJson(value) {
|
|
71
84
|
console.log(JSON.stringify(value, null, process.stdout.isTTY ? 2 : undefined))
|
|
72
85
|
}
|
|
@@ -265,12 +278,31 @@ const CREATE_FLAGS = {
|
|
|
265
278
|
'share-cover': { type: 'string' },
|
|
266
279
|
note: { type: 'string' },
|
|
267
280
|
'require-group': { type: 'boolean' },
|
|
281
|
+
qr: { type: 'string' },
|
|
282
|
+
'qr-out': { type: 'string' },
|
|
268
283
|
}
|
|
269
284
|
|
|
270
285
|
function parseCreateValues(argv) {
|
|
271
286
|
return parseArgs({ strict: false, allowPositionals: true, args: argv, options: CREATE_FLAGS }).values
|
|
272
287
|
}
|
|
273
288
|
|
|
289
|
+
/** --qr / --qr-out 的组合校验放在任何副作用之前 */
|
|
290
|
+
function resolveQrOptions(values) {
|
|
291
|
+
const mode = str(values, 'qr') ?? 'ansi'
|
|
292
|
+
const out = str(values, 'qr-out')
|
|
293
|
+
if (mode !== 'ansi' && mode !== 'svg' && mode !== 'off') {
|
|
294
|
+
const err = new Error('--qr 只支持 ansi / svg / off')
|
|
295
|
+
err.exitCode = 2
|
|
296
|
+
throw err
|
|
297
|
+
}
|
|
298
|
+
if (mode === 'svg' && !out) {
|
|
299
|
+
const err = new Error('--qr svg 需要配 --qr-out <文件>')
|
|
300
|
+
err.exitCode = 2
|
|
301
|
+
throw err
|
|
302
|
+
}
|
|
303
|
+
return { mode, out }
|
|
304
|
+
}
|
|
305
|
+
|
|
274
306
|
function findMissing(values) {
|
|
275
307
|
const missing = []
|
|
276
308
|
if (!str(values, 'mode')) missing.push('--mode random|equal')
|
|
@@ -459,6 +491,7 @@ async function cmdCheck(argv) {
|
|
|
459
491
|
|
|
460
492
|
async function cmdCreate(argv) {
|
|
461
493
|
const values = parseCreateValues(argv)
|
|
494
|
+
const qrOptions = resolveQrOptions(values)
|
|
462
495
|
const interactive = process.stdin.isTTY === true
|
|
463
496
|
|
|
464
497
|
// 向导补齐仅限"真命令行直跑"(bin 入口打 ACT01_CLI_INVOKED 标记);
|
|
@@ -546,19 +579,62 @@ async function cmdCreate(argv) {
|
|
|
546
579
|
return 1
|
|
547
580
|
}
|
|
548
581
|
|
|
549
|
-
const result = r.rows?.[0]?.result
|
|
582
|
+
const result = parseJsonResult(r.rows?.[0]?.result)
|
|
583
|
+
const packet = result && typeof result === 'object' && result.packet && typeof result.packet === 'object'
|
|
584
|
+
? (result.packet.data && typeof result.packet.data === 'object' ? result.packet.data : result.packet)
|
|
585
|
+
: null
|
|
586
|
+
const packetNo = String(packet?.packet_no || '').trim()
|
|
587
|
+
const balanceAfter = balance - input.totalPoints
|
|
588
|
+
|
|
589
|
+
// 二维码:内容走小程序 goto 协议,扫到直接进领红包页(不依赖域名/微信后台规则)
|
|
590
|
+
const qrMode = qrOptions.mode
|
|
591
|
+
const qrOut = qrOptions.out
|
|
592
|
+
const qrContent = packetNo ? claimTarget(packetNo) : ''
|
|
593
|
+
let qr = null
|
|
594
|
+
let qrPath = null
|
|
595
|
+
if (qrContent) {
|
|
596
|
+
try {
|
|
597
|
+
qr = encode(qrContent)
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.error(`warn: 二维码生成失败 (${error.message});红包已创建`)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (qr && qrOut) {
|
|
603
|
+
const target = expandPath(qrOut)
|
|
604
|
+
const text = /\.svg$/i.test(target) ? renderSvg(qr) : renderAnsi(qr, { color: false })
|
|
605
|
+
try {
|
|
606
|
+
writeFileSync(target, `${text}\n`)
|
|
607
|
+
qrPath = target
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error(`warn: 二维码写入失败 (${error.message});红包已创建`)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
550
613
|
if (values.json === true) {
|
|
551
|
-
printJson({
|
|
614
|
+
printJson({
|
|
615
|
+
ok: true,
|
|
616
|
+
packetNo: packetNo || null,
|
|
617
|
+
result,
|
|
618
|
+
balanceBefore: balance,
|
|
619
|
+
balanceAfter,
|
|
620
|
+
qr: qrContent ? { content: qrContent, format: qrMode, path: qrPath } : null,
|
|
621
|
+
})
|
|
552
622
|
} else {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
)
|
|
560
|
-
console.
|
|
623
|
+
console.log(`红包编号 ${packetNo || '(未返回)'}`)
|
|
624
|
+
if (packet) {
|
|
625
|
+
const modeText = packet.mode === 'equal' ? '均分' : '随机'
|
|
626
|
+
console.log(`红包 ${modeText} · ${packet.total_points} 积分 / ${packet.count} 份 · ${packet.status || 'active'}`)
|
|
627
|
+
}
|
|
628
|
+
console.log(`余额变化 ${balance} → ${balanceAfter}`)
|
|
629
|
+
if (qrContent) console.log(`二维码内容 ${qrContent}`)
|
|
630
|
+
if (!packetNo) console.log(`结果 ${typeof result === 'string' ? result : JSON.stringify(result)}`)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (qr && qrMode === 'ansi' && !qrPath) {
|
|
634
|
+
process.stderr.write(`${renderAnsi(qr, { color: process.env.NO_COLOR === undefined })}\n`)
|
|
561
635
|
}
|
|
636
|
+
if (qrPath) console.error(`二维码已写入: ${qrPath}`)
|
|
637
|
+
console.error('hint: deducted; recall is not supported — lifecycle is TTL-driven')
|
|
562
638
|
return 0
|
|
563
639
|
}
|
|
564
640
|
|
package/src/mcp.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { VERSION } from './version.js'
|
|
15
15
|
import { loadConfig } from './store.js'
|
|
16
16
|
import { resolveApikey, execPgsql, gatewayBase } from './tcb.js'
|
|
17
|
-
import { normalizeCreateRedpacket, buildCreateSql } from './redpacket.js'
|
|
17
|
+
import { normalizeCreateRedpacket, buildCreateSql, claimTarget } from './redpacket.js'
|
|
18
18
|
|
|
19
19
|
const SERVER_INFO = { name: 'act-01', version: VERSION }
|
|
20
20
|
const SUPPORTED = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07']
|
|
@@ -80,6 +80,22 @@ function needConfig() {
|
|
|
80
80
|
return { config, apikey }
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** jsonb 可能被网关当字符串回;顺手带回小程序扫码入口(mp://) */
|
|
84
|
+
function withClaimTarget(raw) {
|
|
85
|
+
let result = raw
|
|
86
|
+
if (typeof raw === 'string') {
|
|
87
|
+
try {
|
|
88
|
+
result = JSON.parse(raw)
|
|
89
|
+
} catch {
|
|
90
|
+
result = raw
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!result || typeof result !== 'object') return result
|
|
94
|
+
|
|
95
|
+
const packetNo = String(result.packet?.data?.packet_no || result.packet?.packet_no || '').trim()
|
|
96
|
+
return packetNo ? { ...result, qr: { content: claimTarget(packetNo) } } : result
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
async function callTool(name, args) {
|
|
84
100
|
if (name === 'redpacket_create' || name === 'redpacket_check') {
|
|
85
101
|
const checkOnly = name === 'redpacket_check'
|
|
@@ -100,7 +116,7 @@ async function callTool(name, args) {
|
|
|
100
116
|
err.gateway = true
|
|
101
117
|
throw err
|
|
102
118
|
}
|
|
103
|
-
return r.rows?.[0]?.result ?? r.raw
|
|
119
|
+
return withClaimTarget(r.rows?.[0]?.result ?? r.raw)
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
if (name === 'balance') {
|
package/src/qr.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
qr.js — 二维码编码与渲染(能力型,零依赖)
|
|
3
|
+
|
|
4
|
+
取用:
|
|
5
|
+
import { encode, renderAnsi, renderSvg } from './qr.js'
|
|
6
|
+
const qr = encode('mp://pages/act-01/open/index?packet_no=2000000001')
|
|
7
|
+
process.stderr.write(renderAnsi(qr) + '\n')
|
|
8
|
+
|
|
9
|
+
口径:
|
|
10
|
+
byte 模式 / 纠错 M / version 1..10 自适应 / 8 种 mask 按标准罚分选最优
|
|
11
|
+
只覆盖本 CLI 需要的长度(≤213 字节);超出直接抛错,不静默截断
|
|
12
|
+
ANSI 用 ▀ 半块 + 真彩(白底黑块);color=false 时退成 ██/空格(可写文件)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const EC_M = 0 // 纠错等级 M 的 format 位
|
|
16
|
+
const BLOCKS = {
|
|
17
|
+
1: [1, 26, 16],
|
|
18
|
+
2: [1, 44, 28],
|
|
19
|
+
3: [1, 70, 44],
|
|
20
|
+
4: [2, 50, 32],
|
|
21
|
+
5: [2, 67, 43],
|
|
22
|
+
6: [4, 43, 27],
|
|
23
|
+
7: [4, 49, 31],
|
|
24
|
+
8: [2, 60, 38, 2, 61, 39],
|
|
25
|
+
9: [3, 58, 36, 2, 59, 37],
|
|
26
|
+
10: [4, 69, 43, 1, 70, 44],
|
|
27
|
+
}
|
|
28
|
+
const ALIGN = {
|
|
29
|
+
1: [], 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30],
|
|
30
|
+
6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50],
|
|
31
|
+
}
|
|
32
|
+
const MAX_VERSION = 10
|
|
33
|
+
|
|
34
|
+
/* ---------- GF(256) + Reed-Solomon ---------- */
|
|
35
|
+
|
|
36
|
+
const EXP = new Uint8Array(512)
|
|
37
|
+
const LOG = new Uint8Array(256)
|
|
38
|
+
{
|
|
39
|
+
let x = 1
|
|
40
|
+
for (let i = 0; i < 255; i += 1) {
|
|
41
|
+
EXP[i] = x
|
|
42
|
+
LOG[x] = i
|
|
43
|
+
x <<= 1
|
|
44
|
+
if (x & 0x100) x ^= 0x11d
|
|
45
|
+
}
|
|
46
|
+
for (let i = 255; i < 512; i += 1) EXP[i] = EXP[i - 255]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const mul = (a, b) => (a === 0 || b === 0 ? 0 : EXP[LOG[a] + LOG[b]])
|
|
50
|
+
|
|
51
|
+
const generatorCache = new Map()
|
|
52
|
+
const generator = (degree) => {
|
|
53
|
+
if (generatorCache.has(degree)) return generatorCache.get(degree)
|
|
54
|
+
let poly = [1]
|
|
55
|
+
for (let i = 0; i < degree; i += 1) {
|
|
56
|
+
const next = new Array(poly.length + 1).fill(0)
|
|
57
|
+
for (let j = 0; j < poly.length; j += 1) {
|
|
58
|
+
next[j] ^= poly[j]
|
|
59
|
+
next[j + 1] ^= mul(poly[j], EXP[i])
|
|
60
|
+
}
|
|
61
|
+
poly = next
|
|
62
|
+
}
|
|
63
|
+
generatorCache.set(degree, poly)
|
|
64
|
+
return poly
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const remainder = (data, gen) => {
|
|
68
|
+
const ecc = new Uint8Array(gen.length - 1)
|
|
69
|
+
for (const byte of data) {
|
|
70
|
+
const factor = byte ^ ecc[0]
|
|
71
|
+
ecc.copyWithin(0, 1)
|
|
72
|
+
ecc[ecc.length - 1] = 0
|
|
73
|
+
for (let i = 0; i < ecc.length; i += 1) ecc[i] ^= mul(gen[i + 1], factor)
|
|
74
|
+
}
|
|
75
|
+
return ecc
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ---------- 数据编码 ---------- */
|
|
79
|
+
|
|
80
|
+
const countBits = (version) => (version <= 9 ? 8 : 16)
|
|
81
|
+
const dataCodewords = (version) => {
|
|
82
|
+
const g = BLOCKS[version]
|
|
83
|
+
let total = 0
|
|
84
|
+
for (let i = 0; i < g.length; i += 3) total += g[i] * g[i + 2]
|
|
85
|
+
return total
|
|
86
|
+
}
|
|
87
|
+
const maxBytes = (version) => Math.floor((dataCodewords(version) * 8 - 4 - countBits(version)) / 8)
|
|
88
|
+
|
|
89
|
+
const pickVersion = (length) => {
|
|
90
|
+
for (let v = 1; v <= MAX_VERSION; v += 1) if (maxBytes(v) >= length) return v
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const buildDataBytes = (bytes, version) => {
|
|
95
|
+
const bits = []
|
|
96
|
+
const push = (value, len) => {
|
|
97
|
+
for (let i = len - 1; i >= 0; i -= 1) bits.push((value >>> i) & 1)
|
|
98
|
+
}
|
|
99
|
+
push(0b0100, 4)
|
|
100
|
+
push(bytes.length, countBits(version))
|
|
101
|
+
for (const byte of bytes) push(byte, 8)
|
|
102
|
+
|
|
103
|
+
const capacity = dataCodewords(version) * 8
|
|
104
|
+
push(0, Math.min(4, capacity - bits.length))
|
|
105
|
+
while (bits.length % 8 !== 0) bits.push(0)
|
|
106
|
+
|
|
107
|
+
const out = []
|
|
108
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
109
|
+
let byte = 0
|
|
110
|
+
for (let j = 0; j < 8; j += 1) byte = (byte << 1) | bits[i + j]
|
|
111
|
+
out.push(byte)
|
|
112
|
+
}
|
|
113
|
+
for (let pad = 0xec; out.length < dataCodewords(version); pad ^= 0xec ^ 0x11) out.push(pad)
|
|
114
|
+
return Uint8Array.from(out)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const buildCodewords = (bytes, version) => {
|
|
118
|
+
const data = buildDataBytes(bytes, version)
|
|
119
|
+
const table = BLOCKS[version]
|
|
120
|
+
const blocks = []
|
|
121
|
+
let offset = 0
|
|
122
|
+
for (let i = 0; i < table.length; i += 3) {
|
|
123
|
+
const count = table[i]
|
|
124
|
+
const ecLen = table[i + 1] - table[i + 2]
|
|
125
|
+
const dataLen = table[i + 2]
|
|
126
|
+
const gen = generator(ecLen)
|
|
127
|
+
for (let n = 0; n < count; n += 1) {
|
|
128
|
+
const chunk = data.slice(offset, offset + dataLen)
|
|
129
|
+
offset += dataLen
|
|
130
|
+
blocks.push({ data: chunk, ec: remainder(chunk, gen) })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const out = []
|
|
135
|
+
const maxData = Math.max(...blocks.map((block) => block.data.length))
|
|
136
|
+
for (let i = 0; i < maxData; i += 1) {
|
|
137
|
+
for (const block of blocks) if (i < block.data.length) out.push(block.data[i])
|
|
138
|
+
}
|
|
139
|
+
const ecLen = blocks[0].ec.length
|
|
140
|
+
for (let i = 0; i < ecLen; i += 1) for (const block of blocks) out.push(block.ec[i])
|
|
141
|
+
return Uint8Array.from(out)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ---------- 矩阵 ---------- */
|
|
145
|
+
|
|
146
|
+
const bitAt = (value, index) => ((value >>> index) & 1) === 1
|
|
147
|
+
|
|
148
|
+
const formatBits = (mask) => {
|
|
149
|
+
const data = (EC_M << 3) | mask
|
|
150
|
+
let rem = data
|
|
151
|
+
for (let i = 0; i < 10; i += 1) rem = (rem << 1) ^ ((rem >>> 9) * 0x537)
|
|
152
|
+
return ((data << 10) | rem) ^ 0x5412
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const versionBits = (version) => {
|
|
156
|
+
let rem = version
|
|
157
|
+
for (let i = 0; i < 12; i += 1) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25)
|
|
158
|
+
return (version << 12) | rem
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const blank = (version) => {
|
|
162
|
+
const size = version * 4 + 17
|
|
163
|
+
return {
|
|
164
|
+
size,
|
|
165
|
+
modules: Array.from({ length: size }, () => new Array(size).fill(false)),
|
|
166
|
+
isFunction: Array.from({ length: size }, () => new Array(size).fill(false)),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const drawBase = (version) => {
|
|
171
|
+
const state = blank(version)
|
|
172
|
+
const { size, modules, isFunction } = state
|
|
173
|
+
const set = (row, col, dark) => {
|
|
174
|
+
modules[row][col] = dark
|
|
175
|
+
isFunction[row][col] = true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const finder = (row0, col0) => {
|
|
179
|
+
for (let dr = -1; dr <= 7; dr += 1) {
|
|
180
|
+
for (let dc = -1; dc <= 7; dc += 1) {
|
|
181
|
+
const row = row0 + dr
|
|
182
|
+
const col = col0 + dc
|
|
183
|
+
if (row < 0 || row >= size || col < 0 || col >= size) continue
|
|
184
|
+
const dark =
|
|
185
|
+
(dr >= 0 && dr <= 6 && (dc === 0 || dc === 6)) ||
|
|
186
|
+
(dc >= 0 && dc <= 6 && (dr === 0 || dr === 6)) ||
|
|
187
|
+
(dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4)
|
|
188
|
+
set(row, col, dark)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
finder(0, 0)
|
|
193
|
+
finder(0, size - 7)
|
|
194
|
+
finder(size - 7, 0)
|
|
195
|
+
|
|
196
|
+
for (let i = 8; i < size - 8; i += 1) {
|
|
197
|
+
const dark = i % 2 === 0
|
|
198
|
+
set(6, i, dark)
|
|
199
|
+
set(i, 6, dark)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const positions = ALIGN[version]
|
|
203
|
+
// 只跳过与三个定位图形重叠的中心(6,6)/(6,size-7)/(size-7,6);
|
|
204
|
+
// 落在 timing 行/列上的校正图形要画,并覆盖对应 timing 模块
|
|
205
|
+
const onFinder = (row, col) =>
|
|
206
|
+
(row === 6 && col === 6) ||
|
|
207
|
+
(row === 6 && col === size - 7) ||
|
|
208
|
+
(row === size - 7 && col === 6)
|
|
209
|
+
for (const row of positions) {
|
|
210
|
+
for (const col of positions) {
|
|
211
|
+
if (onFinder(row, col)) continue
|
|
212
|
+
for (let dr = -2; dr <= 2; dr += 1) {
|
|
213
|
+
for (let dc = -2; dc <= 2; dc += 1) {
|
|
214
|
+
set(row + dr, col + dc, Math.max(Math.abs(dr), Math.abs(dc)) !== 1)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
set(size - 8, 8, true)
|
|
221
|
+
drawFormat(state, 0)
|
|
222
|
+
if (version >= 7) {
|
|
223
|
+
const bits = versionBits(version)
|
|
224
|
+
for (let i = 0; i < 18; i += 1) {
|
|
225
|
+
const dark = bitAt(bits, i)
|
|
226
|
+
const a = size - 11 + (i % 3)
|
|
227
|
+
const b = Math.floor(i / 3)
|
|
228
|
+
set(b, a, dark)
|
|
229
|
+
set(a, b, dark)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return state
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function drawFormat(state, mask) {
|
|
236
|
+
const { size, modules, isFunction } = state
|
|
237
|
+
const set = (row, col, dark) => {
|
|
238
|
+
modules[row][col] = dark
|
|
239
|
+
isFunction[row][col] = true
|
|
240
|
+
}
|
|
241
|
+
const bits = formatBits(mask)
|
|
242
|
+
for (let i = 0; i <= 5; i += 1) set(i, 8, bitAt(bits, i))
|
|
243
|
+
set(7, 8, bitAt(bits, 6))
|
|
244
|
+
set(8, 8, bitAt(bits, 7))
|
|
245
|
+
set(8, 7, bitAt(bits, 8))
|
|
246
|
+
for (let i = 9; i < 15; i += 1) set(8, 14 - i, bitAt(bits, i))
|
|
247
|
+
for (let i = 0; i < 8; i += 1) set(8, size - 1 - i, bitAt(bits, i))
|
|
248
|
+
for (let i = 8; i < 15; i += 1) set(size - 15 + i, 8, bitAt(bits, i))
|
|
249
|
+
set(size - 8, 8, true)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const placeData = (state, codewords) => {
|
|
253
|
+
const { size, modules, isFunction } = state
|
|
254
|
+
const total = codewords.length * 8
|
|
255
|
+
let index = 0
|
|
256
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
257
|
+
if (right === 6) right = 5
|
|
258
|
+
const upward = ((right + 1) & 2) === 0
|
|
259
|
+
for (let vert = 0; vert < size; vert += 1) {
|
|
260
|
+
for (let j = 0; j < 2; j += 1) {
|
|
261
|
+
const col = right - j
|
|
262
|
+
const row = upward ? size - 1 - vert : vert
|
|
263
|
+
if (isFunction[row][col] || index >= total) continue
|
|
264
|
+
modules[row][col] = bitAt(codewords[index >>> 3], 7 - (index & 7))
|
|
265
|
+
index += 1
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const maskHit = (mask, row, col) => {
|
|
272
|
+
switch (mask) {
|
|
273
|
+
case 0: return (row + col) % 2 === 0
|
|
274
|
+
case 1: return row % 2 === 0
|
|
275
|
+
case 2: return col % 3 === 0
|
|
276
|
+
case 3: return (row + col) % 3 === 0
|
|
277
|
+
case 4: return (Math.floor(row / 2) + Math.floor(col / 3)) % 2 === 0
|
|
278
|
+
case 5: return ((row * col) % 2) + ((row * col) % 3) === 0
|
|
279
|
+
case 6: return (((row * col) % 2) + ((row * col) % 3)) % 2 === 0
|
|
280
|
+
default: return (((row + col) % 2) + ((row * col) % 3)) % 2 === 0
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const cloneModules = (modules) => modules.map((row) => row.slice())
|
|
285
|
+
|
|
286
|
+
const applyMask = (modules, isFunction, mask) => {
|
|
287
|
+
const size = modules.length
|
|
288
|
+
for (let row = 0; row < size; row += 1) {
|
|
289
|
+
for (let col = 0; col < size; col += 1) {
|
|
290
|
+
if (!isFunction[row][col] && maskHit(mask, row, col)) modules[row][col] = !modules[row][col]
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const PATTERNS = [
|
|
296
|
+
[1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0],
|
|
297
|
+
[0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1],
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
const penalty = (modules) => {
|
|
301
|
+
const size = modules.length
|
|
302
|
+
let score = 0
|
|
303
|
+
|
|
304
|
+
const runScore = (get) => {
|
|
305
|
+
for (let a = 0; a < size; a += 1) {
|
|
306
|
+
let color = get(a, 0)
|
|
307
|
+
let run = 1
|
|
308
|
+
for (let b = 1; b < size; b += 1) {
|
|
309
|
+
const next = get(a, b)
|
|
310
|
+
if (next === color) run += 1
|
|
311
|
+
else {
|
|
312
|
+
if (run >= 5) score += 3 + (run - 5)
|
|
313
|
+
color = next
|
|
314
|
+
run = 1
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (run >= 5) score += 3 + (run - 5)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
runScore((row, col) => modules[row][col])
|
|
321
|
+
runScore((col, row) => modules[row][col])
|
|
322
|
+
|
|
323
|
+
for (let row = 0; row < size - 1; row += 1) {
|
|
324
|
+
for (let col = 0; col < size - 1; col += 1) {
|
|
325
|
+
const color = modules[row][col]
|
|
326
|
+
if (color === modules[row][col + 1] && color === modules[row + 1][col] && color === modules[row + 1][col + 1]) score += 3
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (let row = 0; row < size; row += 1) {
|
|
331
|
+
for (let col = 0; col <= size - 11; col += 1) {
|
|
332
|
+
for (const pattern of PATTERNS) {
|
|
333
|
+
let hit = true
|
|
334
|
+
for (let k = 0; k < 11; k += 1) {
|
|
335
|
+
if (modules[row][col + k] !== Boolean(pattern[k])) { hit = false; break }
|
|
336
|
+
}
|
|
337
|
+
if (hit) score += 40
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (let col = 0; col < size; col += 1) {
|
|
342
|
+
for (let row = 0; row <= size - 11; row += 1) {
|
|
343
|
+
for (const pattern of PATTERNS) {
|
|
344
|
+
let hit = true
|
|
345
|
+
for (let k = 0; k < 11; k += 1) {
|
|
346
|
+
if (modules[row + k][col] !== Boolean(pattern[k])) { hit = false; break }
|
|
347
|
+
}
|
|
348
|
+
if (hit) score += 40
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let dark = 0
|
|
354
|
+
for (const row of modules) for (const cell of row) if (cell) dark += 1
|
|
355
|
+
const total = size * size
|
|
356
|
+
score += Math.floor(Math.abs((dark * 100) / total - 50) / 5) * 10
|
|
357
|
+
return score
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function encode(text, options = {}) {
|
|
361
|
+
const content = String(text ?? '')
|
|
362
|
+
if (!content) throw new Error('二维码内容为空')
|
|
363
|
+
const bytes = new TextEncoder().encode(content)
|
|
364
|
+
|
|
365
|
+
const version = Number(options.version) || pickVersion(bytes.length)
|
|
366
|
+
if (!version) throw new Error(`内容过长: ${bytes.length} 字节(上限 ${maxBytes(MAX_VERSION)})`)
|
|
367
|
+
if (!BLOCKS[version]) throw new Error(`不支持的二维码版本: ${version}`)
|
|
368
|
+
if (bytes.length > maxBytes(version))
|
|
369
|
+
throw new Error(`内容过长: ${bytes.length} 字节(上限 ${maxBytes(version)})`)
|
|
370
|
+
|
|
371
|
+
const state = drawBase(version)
|
|
372
|
+
placeData(state, buildCodewords(bytes, version))
|
|
373
|
+
|
|
374
|
+
const masks = options.mask === undefined ? [0, 1, 2, 3, 4, 5, 6, 7] : [Number(options.mask)]
|
|
375
|
+
let best = null
|
|
376
|
+
for (const mask of masks) {
|
|
377
|
+
const modules = cloneModules(state.modules)
|
|
378
|
+
applyMask(modules, state.isFunction, mask)
|
|
379
|
+
drawFormat({ size: state.size, modules, isFunction: state.isFunction }, mask)
|
|
380
|
+
const score = penalty(modules)
|
|
381
|
+
if (!best || score < best.score) best = { mask, score, modules }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return { version, size: state.size, mask: best.mask, modules: best.modules }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* ---------- 渲染 ---------- */
|
|
388
|
+
|
|
389
|
+
const cellAt = (modules, size, quiet, row, col) => {
|
|
390
|
+
const r = row - quiet
|
|
391
|
+
const c = col - quiet
|
|
392
|
+
return r >= 0 && r < size && c >= 0 && c < size ? modules[r][c] : false
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function renderAnsi(qr, options = {}) {
|
|
396
|
+
const color = options.color !== false
|
|
397
|
+
const quiet = options.quiet === undefined ? 4 : Number(options.quiet)
|
|
398
|
+
const { modules, size } = qr
|
|
399
|
+
const span = size + quiet * 2
|
|
400
|
+
const lines = []
|
|
401
|
+
|
|
402
|
+
if (!color) {
|
|
403
|
+
for (let row = 0; row < span; row += 1) {
|
|
404
|
+
let line = ''
|
|
405
|
+
for (let col = 0; col < span; col += 1) line += cellAt(modules, size, quiet, row, col) ? '██' : ' '
|
|
406
|
+
lines.push(line)
|
|
407
|
+
}
|
|
408
|
+
return lines.join('\n')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (let row = 0; row < span; row += 2) {
|
|
412
|
+
let line = ''
|
|
413
|
+
let fg = ''
|
|
414
|
+
let bg = ''
|
|
415
|
+
for (let col = 0; col < span; col += 1) {
|
|
416
|
+
const top = cellAt(modules, size, quiet, row, col)
|
|
417
|
+
const bottom = cellAt(modules, size, quiet, row + 1, col)
|
|
418
|
+
const nextFg = top ? '0;0;0' : '255;255;255'
|
|
419
|
+
const nextBg = bottom ? '0;0;0' : '255;255;255'
|
|
420
|
+
if (nextFg !== fg || nextBg !== bg) {
|
|
421
|
+
line += `\x1b[38;2;${nextFg}m\x1b[48;2;${nextBg}m`
|
|
422
|
+
fg = nextFg
|
|
423
|
+
bg = nextBg
|
|
424
|
+
}
|
|
425
|
+
line += '▀'
|
|
426
|
+
}
|
|
427
|
+
lines.push(`${line}\x1b[0m`)
|
|
428
|
+
}
|
|
429
|
+
return lines.join('\n')
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function renderSvg(qr, options = {}) {
|
|
433
|
+
const quiet = options.quiet === undefined ? 4 : Number(options.quiet)
|
|
434
|
+
const scale = options.scale === undefined ? 8 : Number(options.scale)
|
|
435
|
+
const { modules, size } = qr
|
|
436
|
+
const span = size + quiet * 2
|
|
437
|
+
let path = ''
|
|
438
|
+
for (let row = 0; row < size; row += 1) {
|
|
439
|
+
for (let col = 0; col < size; col += 1) {
|
|
440
|
+
if (modules[row][col]) path += `M${col + quiet} ${row + quiet}h1v1h-1z`
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return [
|
|
444
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${span * scale}" height="${span * scale}" viewBox="0 0 ${span} ${span}" shape-rendering="crispEdges">`,
|
|
445
|
+
`<rect width="${span}" height="${span}" fill="#fff"/>`,
|
|
446
|
+
`<path d="${path}" fill="#000"/>`,
|
|
447
|
+
'</svg>',
|
|
448
|
+
].join('\n')
|
|
449
|
+
}
|
package/src/redpacket.js
CHANGED
|
@@ -107,6 +107,11 @@ export function buildCreateSql(input) {
|
|
|
107
107
|
return sql
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** 红包领取入口:小程序 goto 协议,扫到后直接 navigateTo 打开页 */
|
|
111
|
+
export function claimTarget(packetNo) {
|
|
112
|
+
return `mp://pages/act-01/open/index?packet_no=${encodeURIComponent(String(packetNo || '').trim())}`
|
|
113
|
+
}
|
|
114
|
+
|
|
110
115
|
/** 确认卡摘要(提交前逐项列出) */
|
|
111
116
|
export function confirmSummary(input) {
|
|
112
117
|
const lines = [
|
package/src/skill.js
CHANGED
|
@@ -56,11 +56,23 @@ const BODY = [
|
|
|
56
56
|
'2. **确认卡**:执行前列出全部参数,等用户确认',
|
|
57
57
|
'3. **库函数原子扣减**:并发/余额竞争由数据库兜底,不会扣穿',
|
|
58
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
|
+
'4. 把红包编号、余额变化和二维码一起给用户(stderr 默认打印终端二维码)',
|
|
65
|
+
'',
|
|
66
|
+
'余额为 0 属正常:create 会被预检拦下,先让用户到运营台充值。',
|
|
67
|
+
'',
|
|
68
|
+
'> 更轻的日常形态(可选):' + B + 'act-01 mcp' + B + ' 接入支持 MCP 的客户端后,参数规格在工具 schema 里,',
|
|
69
|
+
'> 不必读本文档;客户端审批弹窗即"确认卡"。',
|
|
70
|
+
'',
|
|
59
71
|
'## Agent 行为规则',
|
|
60
72
|
'',
|
|
61
73
|
'1. **先 check 后 create**:参数没把握时,先调 redpacket_check,拿到校验报告再执行',
|
|
62
74
|
'2. **create 前必须向用户确认参数**(mode/total/count/区间/群归属);确认后执行时,**非交互(非 TTY)必须带 --yes**,否则 CLI 会拒绝(MCP 客户端则弹审批)',
|
|
63
|
-
'3. **结果转述只给摘要**:packet_no
|
|
75
|
+
'3. **结果转述只给摘要**:packet_no、余额变化、二维码内容(create 默认在 stderr 打印二维码,用 1010「我的 → 常用 → 扫码支付」扫);不要猜测或编造未返回的字段',
|
|
64
76
|
'4. **失败处理**:错误信息是人话(余额不足/区间错误/整除错误),原样转告用户并给出修正建议;不要重试同参数的 create',
|
|
65
77
|
'5. **金额单位**:内部整数单位,不要做 ×100 或 ÷100 的换算,除非用户明确要求',
|
|
66
78
|
'',
|
|
@@ -78,10 +90,10 @@ const BODY = [
|
|
|
78
90
|
'',
|
|
79
91
|
'## 配置前置',
|
|
80
92
|
'',
|
|
81
|
-
'目标机器需要云开发配置(envId + apiKey)
|
|
82
|
-
'(
|
|
93
|
+
'目标机器需要云开发配置(envId + apiKey)。没有配置时,让用户给你一条加密配置串',
|
|
94
|
+
'(团队用生成页产出),一条命令装好:',
|
|
83
95
|
'',
|
|
84
|
-
B + 'act-01
|
|
96
|
+
B + 'ACT01_EXPORT_PASS="<口令>" act-01 setup --install <技能根目录> "<配置串>"' + B,
|
|
85
97
|
'',
|
|
86
98
|
].join('\n')
|
|
87
99
|
|
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>
|