内容
# 越南银行二维码
通过Playwright将越南银行转账二维码生成为PNG支付卡,遵循**NAPAS QR IBFTTA / EMVCo**标准。
本仓库有两种用法:
- 作为**MCP服务器**通过stdio运行,供代理/客户端调用工具。
- 直接作为[`mcp-server/`](mcp-server/)中的**Python模块**使用。
## 功能
- 生成NAPAS/EMVCo标准的二维码负载,包括`38`标签、`704`货币、`VN`国家和CRC16-CCITT校验。
- 使用HTML模板、`qr-code-styling`和本地Be Vietnam Pro字体渲染PNG银行卡。
- 根据BIN、代码、简称或全名从[`banks.json`](mcp-server/banks.json)查找银行。
- 在[`skills/qr-transfer/`](skills/qr-transfer/)中提供代理技能,指导工作流程并避免常见错误。
## 项目结构
```text
vietnam-bank-qr-mcp/
├── mcp-server/
│ ├── server.py # MCP stdio服务器:FastMCP("napas-qr")
│ ├── qr_generator.py # EMVCo/NAPAS TLV负载 + CRC16
│ ├── renderer.py # Playwright子进程渲染器
│ ├── banks.py # 读取/查找/刷新银行数据和标志
│ ├── banks.json # 本地银行数据
│ ├── package.json # Node依赖:qr-code-styling
│ ├── templates/payment-card.html # UI卡 + 在浏览器中渲染二维码
│ └── assets/ # 银行标志和本地字体
└── skills/qr-transfer/ # 代理技能文档
```
## 要求
- Python 3.11+
- Node.js 18.18+用于`qr-code-styling`
- Python包:`mcp`、`playwright`
- Playwright Chromium(通过`playwright install chromium`安装)或兼容的Chromium二进制文件
如果Playwright的捆绑二进制文件不可用(例如在沙盒/CI中),请设置环境变量指向实际的Chromium:
```bash
export CHROMIUM_PATH=/path/to/chrome
```
如果未设置,[`renderer.py`](mcp-server/renderer.py)将回退到Playwright的捆绑Chromium。
## 安装
在[`mcp-server/`](mcp-server/)目录中运行:
```bash
cd mcp-server
pip3 install mcp playwright
playwright install chromium
npm install
```
`npm install`仅用于在HTML模板[`mcp-server/templates/payment-card.html`](mcp-server/templates/payment-card.html)中安装`qr-code-styling`:
```text
mcp-server/node_modules/qr-code-styling/lib/qr-code-styling.js
```
## MCP服务器
```bash
cd mcp-server
python3 server.py
```
服务器使用stdio传输。MCP客户端配置示例:
```json
{
"mcpServers": {
"napas-qr": {
"command": "python3",
"args": ["/absolute/path/to/vietnam-bank-qr-mcp/mcp-server/server.py"]
}
}
}
```
## MCP工具
[`server.py`](mcp-server/server.py)公开了5种工具:
| 工具 | 用途 |
|---|---|
| `generate_payment_card(bank_bin, account_no, account_name?, amount?, order_id?, payment_content?)` | 生成二维码负载并渲染PNG卡。返回带有`png_path`字段的JSON字符串(PNG文件的绝对路径)。`bank_bin`是6位BIN。 |
| `get_qr_data(bank_bin, account_no, amount?, order_id?, payment_content?)` | 仅生成EMVCo二维码负载,不渲染图像。返回JSON字符串。`bank_bin`是6位BIN。 |
| `search_bank(query)` | 根据名称、简称、代码或BIN查找银行。返回JSON字符串。 |
| `get_bank_list()` | 列出[`banks.json`](mcp-server/banks.json)中的所有银行。返回JSON字符串。 |
| `update_bank_list_tool()` | 从VietQR API刷新银行列表并下载标志。涉及网络写入。 |
`generate_payment_card`返回`png_path`而不是`Image`内容块,以避免通过传输序列化时截断。代理/客户端在需要显示时读取PNG文件。
如果找不到银行或出现API错误,将引发`ToolError` → MCP客户端接收到`isError: true`(不返回JSON错误字符串)。
## Python API
以下示例在[`mcp-server/`](mcp-server/)中运行:
```python
from banks import search_banks
from qr_generator import generate_qr_data, render_payment_card, sanitize_content
from renderer import render_card
bank = search_banks("TPBank")[0]
# 重要:二维码负载使用bank.bin,不使用bank.code。
qr_data = generate_qr_data(
bank_bin=bank.bin,
account_no="0123456789",
amount=1000000,
payment_content=sanitize_content("TEST TRANSFER"),
)
# 方法1:render_payment_card() — 返回PNG文件的绝对路径。
png_path = render_payment_card(
qr_data=qr_data,
bank_name=bank.name,
bank_code=bank.code, # 短代码用于标志。
account_no="0123456789",
account_name="NGUYEN VAN A",
amount=1000000, # 数值VND;渲染器自动格式化。
payment_content="TEST TRANSFER",
bank_short_name=bank.short_name,
bank_full_name=bank.name,
)
print(png_path)
# 方法2:如果需要控制card_data和输出路径,请手动使用render_card()。
card_data = {
"qr_data": qr_data,
"bank_code": bank.code, # 渲染器使用短代码查找标志。
"bank_name": bank.short_name, # 用于标题的短名称。
"bank_full_name": bank.name, # 用于副标题的全名称。
"bank_short_name": bank.short_name,
"account_name": "NGUYEN VAN A",
"account_no": "0123456789",
"payment_content": "TEST TRANSFER",
"amount": 1000000, # 数值VND;0/None将隐藏金额框。
}
path = render_card(card_data, output_path="./qr_card.png")
print(path)
```
如果要获取PNG字节而不是路径,请使用`generate_payment_card()`(封装`render_payment_card()` + 读取文件)。
如果需要检查实际签名而不是猜测:
```bash
cd mcp-server
python3 - <<'PY'
import inspect
from banks import search_banks
from qr_generator import generate_qr_data
from renderer import render_card
print(inspect.signature(search_banks))
print(inspect.signature(generate_qr_data))
print(inspect.signature(render_card))
PY
```
## 渲染管道
```text
银行/账户/金额/内容
-> generate_qr_data():创建EMVCo/NAPAS原始负载 + CRC16
-> renderer.py将card_data写入JSON配置文件(不进行字符串插值)
-> 子进程(sys.executable)启动Playwright,读取配置,注入window.__CARD_DATA__
-> payment-card.html加载本地字体和qr-code-styling.js
-> 浏览器渲染样式化二维码画布
-> Playwright截图body,缩放2倍
-> 返回PNG路径;generate_payment_card()读取为字节
```
Playwright在子进程中故意运行(使用`sys.executable`继承正在运行的Python解释器)。不要将同步Playwright直接放入异步MCP处理程序中,因为可能导致死锁。子进程通过JSON配置文件接收输入,而不是字符串插值,以避免转义错误。
## 常见陷阱
### `bank_bin`(BIN)vs `bank_code`(短代码)
`generate_qr_data()`和MCP工具接受`bank_bin` — 6位BIN,如`970423`。渲染器需要`bank_code`(短代码,如`TPB`)在`card_data`中从[`assets/bank_logos/{code}.png`](mcp-server/assets/bank_logos/)读取标志。
```python
# 二维码负载 — 使用BIN
qr_data = generate_qr_data(bank_bin=bank.bin, ...)
# 卡数据 — 使用短代码查找标志
card_data = {"bank_code": bank.code, ...}
```
### MCP工具名称与Python函数不同
- MCP:`search_bank()`返回JSON。
- Python模块:`search_banks()`返回`Bank`列表。
- MCP:`get_qr_data()`。
- Python模块:`generate_qr_data()`。
### 模板需要`file://`,不能使用`set_content()`
[`payment-card.html`](mcp-server/templates/payment-card.html)加载相对资产:
- [`../assets/fonts/...`](mcp-server/assets/fonts/)
- `../node_modules/qr-code-styling/...`
- 通过渲染器注入的银行标志
因此,渲染器必须使用`page.goto("file://...")`使相对路径生效。
### 截图目标是`body`
渲染器截图`body`,而不是`.card`或`.mac-window`,以保留16px的填充和卡片的阴影/圆角。
### 金额`0`或负数不生成标签`54`
`generate_qr_data(amount=0)`、负数和`None`都会省略二维码负载中的标签`54`。这是故意的行为 — 只有`amount > 0`才会创建金额字段。不会通过真值检查进行“修复”。
### 可选行需要显式`[hidden]` CSS
模板使用弹性行。如果修改CSS,请保留此规则:
```css
.info-row[hidden], .amount-row[hidden] { display: none !important; }
```
如果省略此规则,空行可能会仍然可见,即使JS已设置`hidden = true`。
## 二维码规格约束
- 根商户账户标签是`38`,不是`26`。
- 标签`38`包含AID `A000000727`,支付网络字段`00 = BIN`,`01 = account number`,服务`QRIBFTTA`。
- 货币标签`53`是`704`。
- 国家标签`58`是`VN`。
- 支付内容位于附加数据标签`62`,子标签`08`。
- CRC使用CRC16-CCITT,多项式`0x1021`,初始值`0xFFFF`。
算法详细信息请参见[`skills/qr-transfer/references/qr-algorithm.md`](skills/qr-transfer/references/qr-algorithm.md)。
## 烟雾检查
### 二维码负载
```bash
cd mcp-server
python3 - <<'PY'
from qr_generator import generate_qr_data, sanitize_content
qr = generate_qr_data(
"970423",
"0123456789",
100000,
payment_content=sanitize_content("TEST"),
)
print(qr)
print("starts_ok", qr.startswith("00020101021238"))
print("crc_tag", qr[-8:-4])
print("crc", qr[-4:])
PY
```
预期结果:
- `starts_ok True`
- `crc_tag 6304`
- CRC末尾是4个十六进制字符
### 渲染PNG
```bash
cd mcp-server
python3 - <<'PY'
from banks import search_banks
from qr_generator import generate_qr_data, sanitize_content
from renderer import render_card
bank = search_banks("TPBank")[0]
qr_data = generate_qr_data(bank.bin, "0123456789", 100000, payment_content=sanitize_content("TEST"))
path = render_card({
"qr_data": qr_data,
"bank_code": bank.code,
"bank_name": bank.name,
"account_name": "NGUYEN VAN A",
"account_no": "0123456789",
"payment_content": "TEST",
"amount": "100.000 VND",
}, output_path="./qr_card.png")
print(path)
PY
```
如果在新机器上渲染失败,请检查:
1. 是否已运行`playwright install chromium`。
2. 如果使用Playwright以外的Chromium,是否已设置`CHROMIUM_PATH`。
## 银行数据和标志
[`banks.py`](mcp-server/banks.py)从[`mcp-server/banks.json`](mcp-server/banks.json)读取数据,从[`mcp-server/assets/bank_logos/`](mcp-server/assets/bank_logos/)读取标志。
仅在必要时刷新:
```python
from banks import update_bank_list
update_bank_list()
```
此命令调用`https://api.vietqr.io/v2/banks`并从VietQR下载标志,因此将更改本地文件。
## 代理技能
代理技能位于:
```text
skills/qr-transfer/
```
阅读[`skills/qr-transfer/SKILL.md`](skills/qr-transfer/SKILL.md)以了解代理可见的工作流程,已遇到的陷阱和二维码算法/渲染/设计的参考资料。
## 许可证
MIT。
## 鸣谢
- 银行数据和标志:VietQR (`api.vietqr.io`)
- 二维码样式:`qr-code-styling`
- 字体:Be Vietnam Pro
- 二维码规格:NAPAS QR IBFTTA / EMVCo