@blod-nm/1kuan-act-01-cli 0.1.5 → 0.2.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/README.md +5 -0
- package/package.json +1 -1
- 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 +2 -1
- package/src/tcb.js +4 -2
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -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
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
|
@@ -61,6 +61,7 @@ const BODY = [
|
|
|
61
61
|
'1. ' + B + 'act-01 redpacket check <flags>' + B + ' 拿校验结果(零副作用)',
|
|
62
62
|
'2. 把校验结果逐项复述给用户,等确认',
|
|
63
63
|
'3. 确认后执行:' + B + 'act-01 redpacket create <flags> --yes' + B + '(非交互必须带 ' + B + '--yes' + B + ')',
|
|
64
|
+
'4. 把红包编号、余额变化和二维码一起给用户(stderr 默认打印终端二维码)',
|
|
64
65
|
'',
|
|
65
66
|
'余额为 0 属正常:create 会被预检拦下,先让用户到运营台充值。',
|
|
66
67
|
'',
|
|
@@ -71,7 +72,7 @@ const BODY = [
|
|
|
71
72
|
'',
|
|
72
73
|
'1. **先 check 后 create**:参数没把握时,先调 redpacket_check,拿到校验报告再执行',
|
|
73
74
|
'2. **create 前必须向用户确认参数**(mode/total/count/区间/群归属);确认后执行时,**非交互(非 TTY)必须带 --yes**,否则 CLI 会拒绝(MCP 客户端则弹审批)',
|
|
74
|
-
'3. **结果转述只给摘要**:packet_no
|
|
75
|
+
'3. **结果转述只给摘要**:packet_no、余额变化、二维码内容(create 默认在 stderr 打印二维码,用 1010「我的 → 常用 → 扫码支付」扫);不要猜测或编造未返回的字段',
|
|
75
76
|
'4. **失败处理**:错误信息是人话(余额不足/区间错误/整除错误),原样转告用户并给出修正建议;不要重试同参数的 create',
|
|
76
77
|
'5. **金额单位**:内部整数单位,不要做 ×100 或 ÷100 的换算,除非用户明确要求',
|
|
77
78
|
'',
|
package/src/tcb.js
CHANGED
|
@@ -67,7 +67,9 @@ export async function execPgsql(config, apikey, sql) {
|
|
|
67
67
|
return { ok: false, status: res.status, raw, message: `HTTP ${res.status}: ${detail}` }
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// exec-pgsql 现在回的是裸数组([{...}]);老形状 { rows: [...] } 也一并兼容
|
|
71
|
+
const rows = Array.isArray(raw)
|
|
72
|
+
? raw
|
|
73
|
+
: (raw && typeof raw === 'object' && Array.isArray(raw.rows) ? raw.rows : undefined)
|
|
72
74
|
return { ok: true, status: res.status, rows, raw }
|
|
73
75
|
}
|
package/src/version.js
CHANGED