@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blod-nm/1kuan-act-01-cli",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
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",
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({ ok: true, result, balanceBefore: balance, balanceAfter: balance - input.totalPoints })
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
- printTable(
554
- ['RESULT', '余额变化'],
555
- [[
556
- typeof result === 'string' ? result : JSON.stringify(result),
557
- `${balance} → ${balance - input.totalPoints}`,
558
- ]],
559
- )
560
- console.error('hint: deducted; recall is not supported — lifecycle is TTL-driven')
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
- const rows =
71
- raw && typeof raw === 'object' && Array.isArray(raw.rows) ? raw.rows : undefined
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
@@ -3,4 +3,4 @@
3
3
  * (test/version.test.js 会断言两者一致,防止再漏升)。
4
4
  */
5
5
 
6
- export const VERSION = '0.1.5'
6
+ export const VERSION = '0.2.1'