Skill: hinisal/qrcode-skills (version 1.0.1)

--- name: qrcode-skills description: Generate and decode QR codes locally. Use when the user wants to create a QR code from text/URL, decode/read QR code content from an image, or asks about QR code generation and scanning. --- # QR Code Generation & Decoding 完全本地的二维码生成与解码，无需网络请求。 **声明：** - **依赖均为公开开源库**：所有依赖（Python: zxingcpp、Pillow、openpyxl、qrcode；Node.js: qrcode、qr-scanner-wechat、sharp、xlsx、archiver）均为公开的开源项目，可在 PyPI / npm 上查阅源码与许可证 - **所有行为本地完成**：生成与解码均在用户本地执行，不调用任何远程 API，不上传任何数据；仅当解码输入为远程图片 URL 时，会下载该图片到本地后再解码 如需二维码生成 URL（无需保存文件即可预览）、更好的解码效果，可使用 [qrcode-remote-skills](https://github.com/caoliao/qrcode-remote-skills)：`npx skills add caoliao/qrcode-remote-skills`。 ## 运行时选择：Python 或 Node.js 所有脚本同时提供 Python 和 Node.js 两个版本，功能和参数完全一致。**执行任何脚本前，先确定运行时。** **选择策略（首次执行脚本前完成）：** 1. 检测 Python 是否可用：`python --version`（或 `python3 --version`） 2. 若 Python 不可用，检测 Node.js：`node --version` 3. 两者都有时默认用 Python **依赖检查与自动安装：** 确定运行时后，检查依赖是否已安装。若缺失则自动安装，无需询问用户： - **Python**：检查 `pip list` 中是否包含关键包（如 `Pillow`），若缺失则执行： ```bash pip install -r requirements.txt ``` - **Node.js**：检查 skill 目录下是否存在 `node_modules/`，若缺失则执行： ```bash npm install ``` 安装完成后再执行脚本。若安装失败，提示用户手动安装。 **命令对照表：** | 功能 | Python | Node.js | |------|--------|---------| | 单个生成保存 | `python scripts/generate.py ...` | `node scripts/generate.js ...` | | 单个解码 | `python scripts/decode.py ...` | `node scripts/decode.js ...` | | 批量生成 | `python scripts/batch_generate.py ...` | `node scripts/batch_generate.js ...` | | 批量解码 | `python scripts/batch_decode.py ...` | `node scripts/batch_decode.js ...` | 所有参数名、输出 JSON 格式完全相同，仅将 `python` 替换为 `node`、`.py` 替换为 `.js`。 **差异说明：** - Python 版本地解码使用 `zxingcpp`，输出 `"source": "zxing"` - Node.js 版本地解码使用 [`qr-scanner-wechat`](https://github.com/AntFu/qr-scanner-wechat)（基于 OpenCV + 微信算法，识别率更高），输出 `"source": "wechat-qr"` --- ## 生成二维码 将文本或 URL 编码为二维码图片，保存到本地文件。 ### 参数说明 | 参数 | 必选 | 默认值 | 说明 | |------|------|--------|------| | --data | 是 | - | 二维码中的文本内容，建议不超过 900 字符 | | --output | 是 | - | 本地保存路径 | | --size | 否 | 400x400 | 图片尺寸，格式 `WxH` 或单个整数 | | --format | 否 | png | 输出格式：`png`（位图）或 `svg`（矢量） | | --error-correction | 否 | M | 纠错级别：L(7%) / M(15%) / Q(25%) / H(30%) | | --border | 否 | 2 | 边框宽度（码点为单位） | ### 使用步骤 无论用户是否要求保存到指定路径，都需要执行脚本来生成二维码文件。 1. 获取用户要编码的文本内容 2. 确定保存路径： - 用户指定了路径（如 `保存到 D:\qr.png`、`放到桌面`）→ 使用用户指定路径 - 用户未指定路径 → 默认使用当前工作目录下的 `qrcode.png`（svg 格式用 `qrcode.svg`） 3. 执行脚本生成 ```bash python scripts/generate.py --data <文本内容> --output <保存路径> [--size 400x400] [--format png] [--error-correction M] [--border 2] ``` 脚本输出 JSON： ```json {"file": "D:\\path\\to\\qrcode.png"} ``` 失败时： ```json {"error": "生成失败: ..."} ``` 呈现给用户的格式： ``` 二维码已生成并保存到本地： **本地文件：** <file> ``` ### 示例 用户输入：`帮我生成一个二维码，内容是 https://example.com` 执行：`python scripts/generate.py --data "https://example.com" --output "qrcode.png"` ``` 二维码已生成并保存到本地： **本地文件：** D:\workspace\qrcode.png ``` 用户输入：`生成一个二维码保存到桌面，内容是 Hello World，SVG 格式、最高纠错` 执行：`python scripts/generate.py --data "Hello World" --output "C:\Users\xxx\Desktop\qrcode.svg" --format svg --error-correction H` ``` 二维码已生成并保存到本地： **本地文件：** C:\Users\xxx\Desktop\qrcode.svg ``` ### 批量生成 当用户提供 Excel/CSV/TXT 文件要求批量生成二维码时，使用 `scripts/batch_generate.py`。 **交互流程：** 1. 用户提供批量数据文件 2. 执行脚本 3. 若返回 `need_column`，展示列信息，询问用户选哪一列，加 `--column` 重新执行 4. 成功后向用户报告结果 **列检测逻辑（Excel/CSV）：** - 自动检测：扫描首行表头，匹配关键词（data/text/content/url/内容/文本/数据/链接 等） - 若只有一列，直接使用 - 若无法判断，脚本返回 `need_column` JSON，此时**需要询问用户指定哪一列** **TXT：** 每行一条数据，无需列选择。 ```bash python scripts/batch_generate.py --input <文件> --output-dir <输出目录> [--column <列名或索引>] [--zip] [--size 400] [--format png] [--error-correction M] [--border 2] ``` - `--output-dir`：图片输出目录（必选） - 以索引命名（`1.png`, `2.png`, ...） - `--zip`：打包输出目录为 zip 脚本输出 JSON： ```json {"total": 100, "success": 98, "failed": 2, "output_dir": "D:\\output", "zip_file": "D:\\output.zip", "errors": [...]} ``` 呈现给用户的格式： ``` 批量生成完成：共 <total> 个，成功 <success> 个，失败 <failed> 个 输出目录：<output_dir> ZIP 文件：<zip_file>（仅打包时显示） ``` --- ## 解码二维码 从二维码图片中读取/解码内容，完全本地解码。 ### 前置依赖 首次使用前，需安装依赖（skill 目录下执行）： - **Python**：`pip install -r requirements.txt`（依赖 `zxingcpp` + `Pillow`） - **Node.js**：`npm install`（依赖 `qr-scanner-wechat` + `sharp`） ### 场景一：单张解码 通过 Shell 工具执行 `scripts/decode.py`，支持本地文件和图片 URL： ```bash python scripts/decode.py <图片路径或URL> python scripts/decode.py --file <本地文件路径> python scripts/decode.py --url <图片URL> ``` **用户直接发送图片时的处理：** - 用户在对话中粘贴/拖入/附加图片时，图片会作为附件提供，可通过文件路径访问 - 获取到图片的本地路径后，直接传给 `decode.py --file <路径>` 即可 - 若图片是通过 @ 引用的文件，同样使用其文件路径 **SKILL_DIR 定位**：脚本路径相对于本 skill 目录，执行时需 `cd` 到 skill 目录或使用绝对路径。 脚本输出 JSON，根据 `source` 字段判断解码引擎： ```json {"source": "zxing", "contents": ["解码内容"]} ``` 失败时： ```json {"error": "无法解码: 本地 zxing 未识别到二维码"} ``` 呈现给用户的格式： 单个二维码： ``` 二维码解码结果（via <source>）： - 内容：<解码出的文本> ``` 多个二维码： ``` 二维码解码结果（共识别到 N 个二维码，via <source>）： 1. <内容1> 2. <内容2> ``` ### 场景二：批量解码 当用户提供 Excel/CSV/TXT 文件要求批量解码二维码图片时，使用 `scripts/batch_decode.py`： ```bash python scripts/batch_decode.py --input <文件> [--column <列名或索引>] [--output-txt <输出TXT路径>] ``` **列检测逻辑（Excel/CSV）：** - 自动检测：扫描首行表头，匹配关键词（url/link/image/img/图片/链接/网址/二维码 等） - 若只有一列，直接使用 - 若无法判断，脚本返回 `need_column` JSON，此时**需要询问用户指定哪一列** **TXT：** 每行一个图片路径或 URL，无需列选择。 **默认行为：** 在原文件中新增"解码结果"列（Excel 新增列 / CSV 新增列），直接回写原文件。 **单独输出 TXT：** 用户明确要求时加 `--output-txt` 参数，所有结果按行分隔写入 TXT。 **未解析的图片：** 对应行写入 `未解析到二维码`。 脚本输出 JSON： ```json {"total": 50, "success": 48, "failed": 2, "output_file": "D:\\data.xlsx", "output_txt": null} ``` 需要用户选列时： ```json {"need_column": true, "columns": ["名称", "图片链接", "备注"], "preview": [...], "message": "无法自动判断 URL 列，请指定 --column 参数"} ``` **交互流程：** 1. 执行脚本 2. 若返回 `need_column`，将 `columns` 和 `preview` 展示给用户，询问使用哪一列 3. 用户回答后，加上 `--column` 重新执行 4. 成功后向用户报告结果统计 呈现给用户的格式： ``` 批量解码完成：共 <total> 个，成功 <success> 个，失败 <failed> 个 结果已写入：<output_file> TXT 输出：<output_txt>（仅单独输出时显示） ``` --- ## 注意事项 - **本地优先**：所有操作均在本地完成，无需网络（除非解码的输入是远程图片 URL，此时需下载图片到本地再解码） - 生成二维码始终需要执行脚本，输出为本地文件 - 解码二维码仅使用本地库（Python: zxingcpp / Node: qr-scanner-wechat），不依赖任何远程服务 - data 建议不超过 900 字符 - 批量操作时，若脚本返回 `need_column`，必须先向用户展示列信息并确认后再重新执行 ## 工程结构 ``` qrcode-skills/ ├── SKILL.md # 主指令文件 ├── requirements.txt # Python 依赖 ├── package.json # Node.js 依赖 └── scripts/ ├── generate.py / .js # 单个生成并保存到本地 ├── batch_generate.py / .js # 批量生成图片 ├── decode.py / .js # 单个解码（纯本地） └── batch_decode.py / .js # 批量解码（纯本地） ```

path: package.json
content: {
  "name": "qrcode-skills",
  "version": "2.0.0",
  "description": "QR Code generation & decoding scripts (fully local)",
  "dependencies": {
    "qrcode": "^1.5.0",
    "qr-scanner-wechat": "^0.1.0",
    "sharp": "^0.33.0",
    "xlsx": "^0.18.0",
    "archiver": "^7.0.0"
  }
}


path: README.md
content: English | [中文](README.zh-cn.md)

# QR Code Skills

An agent skill for generating and decoding QR codes.

**Fully local** — no remote API or API key required.

**Declaration:**
- **All dependencies are open-source**: Every dependency (Python: zxingcpp, Pillow, openpyxl, qrcode; Node.js: qrcode, qr-scanner-wechat, sharp, xlsx, archiver) is a publicly available open-source project. Source code and licenses can be found on PyPI / npm.
- **All operations run locally**: Generation and decoding are performed entirely on the user's machine. No remote API calls, no data uploads. The only exception is when decoding a remote image URL; the image is downloaded locally first, then decoded.

For QR code generation URLs (preview without saving files) or better decoding accuracy, use [qrcode-remote-skills](https://github.com/caoliao/qrcode-remote-skills): `npx skills add caoliao/qrcode-remote-skills`.

## Features

| Feature | Description |
|---------|-------------|
| Generate QR code | Encode text/URL into a QR code image, saved locally |
| Decode QR code | Read QR code content from a local image or image URL |
| Batch generate | Generate QR code images locally from Excel/CSV/TXT |
| Batch decode | Batch decode QR codes from Excel/CSV/TXT, write results back |

## Installation

### Via skills CLI (Recommended)

```bash
npx skills add caoliao/qrcode-skills
```

```bash
# Global install (available across projects)
npx skills add caoliao/qrcode-skills -g

# Install for a specific agent
npx skills add caoliao/qrcode-skills -a cursor
npx skills add caoliao/qrcode-skills -a claude-code
```

### Manual Installation

Clone this repository into your agent's skills directory:

```bash
git clone https://github.com/caoliao/qrcode-skills
```

### Install Dependencies (pick one)

The agent will auto-detect your environment and install dependencies on first use. You can also install manually:

**Python**

```bash
cd qrcode-skills
pip install -r requirements.txt
```

Dependencies: `zxingcpp`, `Pillow`, `openpyxl`, `qrcode`

**Node.js (when Python is unavailable)**

```bash
cd qrcode-skills
npm install
```

Dependencies: `qrcode`, `qr-scanner-wechat`, `sharp`, `xlsx`, `archiver`

> Both script sets are functionally identical. The agent auto-detects the available runtime.

### Verify Installation

After installation, simply ask your agent "Generate a QR code for me" to trigger this skill.

## Examples

### Generate a QR Code

> **You:** Generate a QR code for https://example.com

> **AI:** *(runs local generation script)*
>
> QR code generated and saved locally:
>
> **Local file:** `D:\workspace\qrcode.png`

---

### Generate & Save to Specific Path

> **You:** Generate a QR code and save it to the desktop, content is Hello World, SVG format

> **AI:** *(runs local generation script)*
>
> QR code generated and saved locally:
>
> **Local file:** `C:\Users\xxx\Desktop\qrcode.svg`

---

### Decode a QR Code (Local File)

> **You:** Decode this QR code *(paste/drag an image)*

> **AI:** *(gets image path, runs decode script)*
>
> QR code decoded (via zxing):
> - Content: https://example.com

---

### Decode a QR Code (URL)

> **You:** Decode this QR code https://example.com/qr.png

> **AI:** *(downloads image locally, runs decode script)*
>
> QR code decoded (via zxing):
> - Content: https://cli.im

---

### Batch Generate

> **You:** Batch generate QR code images from the link column in products.csv, zip them up

> **AI:** *(runs script)*
>
> Batch generation complete: 200 total, 200 succeeded, 0 failed
> Output directory: `D:\output\qrcodes`
> ZIP file: `D:\output\qrcodes.zip`

---

### Batch Decode

> **You:** Batch decode QR code images in images.xlsx

> **AI:** Detected the following columns. Which column contains the QR code image URLs?
>
> | Index | Column Name |
> |-------|-------------|
> | 0 | Product Name |
> | 1 | Image Link |
> | 2 | Notes |

> **You:** Column 1

> **AI:** *(runs script)*
>
> Batch decode complete: 50 total, 48 succeeded, 2 failed
> Results written to: `D:\data\images.xlsx` (new "Decode Result" column added)

## Project Structure

```
qrcode-skills/
├── README.md                   # English documentation (this file)
├── README.zh-cn.md             # Chinese documentation
├── SKILL.md                    # Agent skill instructions
├── requirements.txt            # Python dependencies
├── package.json                # Node.js dependencies
└── scripts/
    ├── generate.py / .js       # Single QR code generation & save
    ├── batch_generate.py / .js # Batch generation (images)
    ├── decode.py / .js         # Single decode (fully local)
    └── batch_decode.py / .js   # Batch decode (fully local)
```

## Technical Details

- **Dual runtime**: All scripts are available in both Python and Node.js with identical parameters and output formats
- **Generation**: Uses local libraries (Python: `qrcode` / Node: `qrcode`) to generate QR code images directly
- **Decoding**: Uses local libraries (Python: `zxingcpp` / Node: `qr-scanner-wechat`) for decoding, no remote dependency
- **Batch operations**: Supports Excel (.xlsx), CSV, TXT input; auto-detects data columns, prompts user when ambiguous
- **Fully offline**: All generation and decoding operations run locally with no network required (unless decoding input is a remote image URL)
- **Open-source only**: All dependencies are open-source libraries; no proprietary or closed-source components


path: README.zh-cn.md
content: [English](README.md) | 中文

# QR Code Skills

为 Agent 赋予二维码生成与解码能力的 Skill 合集。

**完全本地运行**，无需任何远程 API 或 API Key。

**声明：**
- **依赖均为公开开源库**：所有依赖（Python: zxingcpp、Pillow、openpyxl、qrcode；Node.js: qrcode、qr-scanner-wechat、sharp、xlsx、archiver）均为公开的开源项目，可在 PyPI / npm 上查阅源码与许可证
- **所有行为本地完成**：生成与解码均在用户本地执行，不调用任何远程 API，不上传任何数据；仅当解码输入为远程图片 URL 时，会下载该图片到本地后再解码

如需二维码生成 URL（无需保存文件即可预览）、更好的解码效果，可使用 [qrcode-remote-skills](https://github.com/caoliao/qrcode-remote-skills)：`npx skills add caoliao/qrcode-remote-skills`。

## 功能概览

| 功能 | 说明 |
|------|------|
| 生成二维码 | 将文本/URL 转为二维码图片，保存到本地 |
| 解码二维码 | 从本地图片或图片 URL 识别二维码内容 |
| 批量生成 | 从 Excel/CSV/TXT 批量生成二维码图片到本地 |
| 批量解码 | 从 Excel/CSV/TXT 批量解码二维码，结果回写原文件 |

## 安装

### 通过 skills CLI 安装（推荐）

```bash
npx skills add caoliao/qrcode-skills
```

```bash
# 全局安装（跨项目可用）
npx skills add caoliao/qrcode-skills -g

# 安装到指定 Agent
npx skills add caoliao/qrcode-skills -a cursor
npx skills add caoliao/qrcode-skills -a claude-code
```

### 手动安装

直接将本项目 clone 到 Agent 的 skills 目录下：

```bash
git clone https://github.com/caoliao/qrcode-skills
```

### 安装依赖（二选一）

首次使用时 Agent 会自动检测环境并安装依赖，也可以手动安装：

**Python 环境**

```bash
cd qrcode-skills
pip install -r requirements.txt
```

依赖：`zxingcpp`、`Pillow`、`openpyxl`、`qrcode`

**Node.js 环境（无 Python 时）**

```bash
cd qrcode-skills
npm install
```

依赖：`qrcode`、`qr-scanner-wechat`、`sharp`、`xlsx`、`archiver`

> 两套脚本功能和参数完全一致，Agent 会自动检测环境并选择可用的运行时。

### 验证安装

安装完成后，直接发送给 Agent "帮我生成一个二维码"即可触发此 Skill。

## 示例问答

### 生成二维码

> **你：** 帮我生成一个二维码，内容是 https://example.com

> **AI：** *(执行本地生成脚本)*
>
> 二维码已生成并保存到本地：
>
> **本地文件：** `D:\workspace\qrcode.png`

---

### 生成并保存到指定路径

> **你：** 生成一个二维码保存到桌面，内容是 Hello World，SVG 格式

> **AI：** *(执行本地生成脚本)*
>
> 二维码已生成并保存到本地：
>
> **本地文件：** `C:\Users\xxx\Desktop\qrcode.svg`

---

### 解码二维码（本地文件）

> **你：** 帮我识别这个二维码 *(粘贴/拖入一张二维码图片)*

> **AI：** *(获取图片路径，执行解码脚本)*
>
> 二维码解码结果（via zxing）：
> - 内容：https://example.com

---

### 解码二维码（URL）

> **你：** 帮我识别这个二维码 https://example.com/qr.png

> **AI：** *(下载图片到本地，执行解码脚本)*
>
> 二维码解码结果（via zxing）：
> - 内容：https://cli.im

---

### 批量生成

> **你：** 把 products.csv 里的链接列批量生成二维码图片，打包成 zip

> **AI：** *(执行脚本)*
>
> 批量生成完成：共 200 个，成功 200 个，失败 0 个
> 输出目录：`D:\output\qrcodes`
> ZIP 文件：`D:\output\qrcodes.zip`

---

### 批量解码

> **你：** 帮我批量解码 images.xlsx 里的二维码图片

> **AI：** 检测到以下列，请问二维码图片 URL 在哪一列？
>
> | 列索引 | 列名 |
> |--------|------|
> | 0 | 产品名称 |
> | 1 | 图片链接 |
> | 2 | 备注 |

> **你：** 第 1 列

> **AI：** *(执行脚本)*
>
> 批量解码完成：共 50 个，成功 48 个，失败 2 个
> 结果已写入：`D:\data\images.xlsx`（新增"解码结果"列）

## 工程结构

```
qrcode-skills/
├── README.md                   # English documentation
├── README.zh-cn.md             # 中文文档（本文件）
├── SKILL.md                    # Agent Skill 主指令
├── requirements.txt            # Python 依赖
├── package.json                # Node.js 依赖
└── scripts/
    ├── generate.py / .js       # 单个生成并保存到本地
    ├── batch_generate.py / .js # 批量生成图片
    ├── decode.py / .js         # 单个解码（纯本地）
    └── batch_decode.py / .js   # 批量解码（纯本地）
```

## 技术说明

- **双运行时**：所有脚本同时提供 Python 和 Node.js 版本，参数和输出格式完全一致
- **生成**：使用本地库（Python: `qrcode` / Node: `qrcode`）直接生成二维码图片
- **解码**：使用本地库（Python: `zxingcpp` / Node: `qr-scanner-wechat`）解码，无远程依赖
- **批量操作**：支持 Excel(.xlsx)、CSV、TXT 输入；自动检测数据列，无法判断时交互询问
- **完全离线**：所有生成和解码操作均在本地完成，无需网络连接（除非解码输入为远程图片 URL）
- **全部开源**：所有依赖均为公开的开源库，无任何闭源或专有组件


path: requirements.txt
content: zxingcpp
Pillow
openpyxl
qrcode


path: scripts/batch_decode.js
content: #!/usr/bin/env node
/**
 * 批量解码二维码 - 本地解码 (Node.js 版)
 *
 * 用法:
 *   node scripts/batch_decode.js --input <文件> [--column <列>] [--output-txt <路径>]
 *
 * 输出 JSON 格式与 Python 版一致。
 */

const https = require("https");
const http = require("http");
const fs = require("fs");
const os = require("os");
const path = require("path");

const FAIL = "未解析到二维码";

// ── 参数解析 ──────────────────────────────────────────────

function parseArgs(argv) {
  const args = { input: null, column: null, outputTxt: null };
  for (let i = 2; i < argv.length; i++) {
    switch (argv[i]) {
      case "--input": args.input = argv[++i]; break;
      case "--column": args.column = argv[++i]; break;
      case "--output-txt": args.outputTxt = argv[++i]; break;
    }
  }
  return args;
}

// ── HTTP 工具 ──────────────────────────────────────────────

function isUrl(s) { return s.startsWith("http://") || s.startsWith("https://"); }

function downloadToTemp(url) {
  return new Promise((resolve, reject) => {
    const ext = path.extname(url.split("?")[0]) || ".png";
    const tmp = path.join(os.tmpdir(), `qr_${Date.now()}_${Math.random().toString(36).slice(2)}${ext}`);
    const mod = url.startsWith("https") ? https : http;
    const file = fs.createWriteStream(tmp);
    mod.get(url, (res) => {
      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
        file.close(); fs.unlinkSync(tmp);
        return downloadToTemp(res.headers.location).then(resolve, reject);
      }
      res.pipe(file);
      file.on("finish", () => { file.close(); resolve(tmp); });
    }).on("error", (e) => { file.close(); if (fs.existsSync(tmp)) fs.unlinkSync(tmp); reject(e); });
  });
}

// ── 解码函数 ──────────────────────────────────────────────

async function tryWechatQr(source) {
  try {
    const { scan } = await import("qr-scanner-wechat");
    const sharp = require("sharp");

    let imgPath = source;
    let tmpPath = null;
    if (isUrl(source)) {
      tmpPath = await downloadToTemp(source);
      imgPath = tmpPath;
    }
    try {
      const { data, info } = await sharp(imgPath)
        .ensureAlpha()
        .raw()
        .toBuffer({ resolveWithObject: true });
      const result = await scan({
        data: Uint8ClampedArray.from(data),
        width: info.width,
        height: info.height,
      });
      return result?.text || null;
    } finally {
      if (tmpPath && fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
    }
  } catch {
    return null;
  }
}

async function decodeSingle(source) {
  const local = await tryWechatQr(source);
  return local || FAIL;
}

// ── 文件读写 ──────────────────────────────────────────────

const DECODE_KEYWORDS = ["url", "link", "image", "img", "图片", "链接", "网址", "二维码"];

function readTxt(fp) { return fs.readFileSync(fp, "utf-8").split("\n").map((l) => l.trim()).filter(Boolean); }

function parseCsvLine(line) {
  const result = []; let current = "", inQ = false;
  for (const ch of line) { if (ch === '"') inQ = !inQ; else if (ch === "," && !inQ) { result.push(current.trim()); current = ""; } else current += ch; }
  result.push(current.trim()); return result;
}

function readCsv(fp) { return fs.readFileSync(fp, "utf-8").replace(/^\uFEFF/, "").split("\n").filter(Boolean).map(parseCsvLine); }

function readExcel(fp) {
  const XLSX = require("xlsx");
  const wb = XLSX.readFile(fp);
  return XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1, defval: "" }).map((r) => r.map((c) => String(c ?? "")));
}

function resolveCol(headers, column) {
  if (column != null) {
    const num = parseInt(column);
    if (!isNaN(num) && num >= 0 && num < headers.length) return num;
    const low = String(column).toLowerCase().trim();
    for (let i = 0; i < headers.length; i++) { if (headers[i].trim().toLowerCase() === low) return i; }
    return null;
  }
  for (let i = 0; i < headers.length; i++) {
    const h = headers[i].trim().toLowerCase();
    for (const kw of DECODE_KEYWORDS) { if (h.includes(kw)) return i; }
  }
  return headers.length === 1 ? 0 : null;
}

async function processTxt(inputPath, outputTxt) {
  const lines = readTxt(inputPath);
  const results = [];
  let success = 0;
  for (const line of lines) {
    const d = await decodeSingle(line);
    results.push(d);
    if (d !== FAIL) success++;
  }
  const stem = path.basename(inputPath, path.extname(inputPath));
  const outPath = outputTxt || path.join(path.dirname(inputPath), `${stem}_decoded.txt`);
  fs.writeFileSync(outPath, results.join("\n"), "utf-8");
  return { total: lines.length, success, failed: lines.length - success, output_file: path.resolve(outPath), output_txt: path.resolve(outPath) };
}

async function processCsv(inputPath, column, outputTxt) {
  const rows = readCsv(inputPath);
  if (!rows.length) return { error: "CSV 文件为空" };
  const headers = rows[0];
  const dataRows = rows.slice(1);
  const colIdx = resolveCol(headers, column);
  if (colIdx == null) return { need_column: true, columns: headers, preview: rows.slice(0, 6), message: column == null ? "无法自动判断 URL 列，请指定 --column 参数" : `找不到列 '${column}'` };

  const decoded = [];
  let success = 0;
  for (const row of dataRows) {
    const url = (row[colIdx] || "").trim();
    const d = url ? await decodeSingle(url) : FAIL;
    decoded.push(d);
    if (d !== FAIL) success++;
  }

  if (outputTxt) {
    fs.writeFileSync(outputTxt, decoded.join("\n"), "utf-8");
    return { total: dataRows.length, success, failed: dataRows.length - success, output_file: path.resolve(inputPath), output_txt: path.resolve(outputTxt) };
  }

  headers.push("解码结果");
  dataRows.forEach((r, i) => r.push(decoded[i]));
  const csvContent = [headers, ...dataRows].map((r) => r.map((c) => `"${String(c).replace(/"/g, '""')}"`).join(",")).join("\n");
  fs.writeFileSync(inputPath, "\uFEFF" + csvContent, "utf-8");
  return { total: dataRows.length, success, failed: dataRows.length - success, output_file: path.resolve(inputPath), output_txt: null };
}

async function processExcel(inputPath, column, outputTxt) {
  const XLSX = require("xlsx");
  const rows = readExcel(inputPath);
  if (!rows.length) return { error: "Excel 文件为空" };
  const headers = rows[0];
  const dataRows = rows.slice(1);
  const colIdx = resolveCol(headers, column);
  if (colIdx == null) return { need_column: true, columns: headers, preview: rows.slice(0, 6), message: column == null ? "无法自动判断 URL 列，请指定 --column 参数" : `找不到列 '${column}'` };

  const decoded = [];
  let success = 0;
  for (const row of dataRows) {
    const url = (row[colIdx] || "").trim();
    const d = url ? await decodeSingle(url) : FAIL;
    decoded.push(d);
    if (d !== FAIL) success++;
  }

  if (outputTxt) {
    fs.writeFileSync(outputTxt, decoded.join("\n"), "utf-8");
    return { total: dataRows.length, success, failed: dataRows.length - success, output_file: path.resolve(inputPath), output_txt: path.resolve(outputTxt) };
  }

  headers.push("解码结果");
  dataRows.forEach((r, i) => r.push(decoded[i]));
  const allRows = [headers, ...dataRows];
  const wb = XLSX.utils.book_new();
  const ws = XLSX.utils.aoa_to_sheet(allRows);
  XLSX.utils.book_append_sheet(wb, ws);
  XLSX.writeFile(wb, inputPath);
  return { total: dataRows.length, success, failed: dataRows.length - success, output_file: path.resolve(inputPath), output_txt: null };
}

// ── 入口 ──────────────────────────────────────────────

async function main() {
  const args = parseArgs(process.argv);
  if (!args.input) { console.log(JSON.stringify({ error: "用法: node batch_decode.js --input <文件> [选项]" })); process.exit(1); }

  const ext = path.extname(args.input).toLowerCase();
  let result;
  if (ext === ".txt") result = await processTxt(args.input, args.outputTxt);
  else if (ext === ".csv") result = await processCsv(args.input, args.column, args.outputTxt);
  else if (ext === ".xlsx" || ext === ".xls") result = await processExcel(args.input, args.column, args.outputTxt);
  else result = { error: `不支持的文件格式: ${ext}，支持 txt/csv/xlsx` };

  console.log(JSON.stringify(result));
  if (result.error) process.exit(1);
}

main();


path: scripts/batch_decode.py
content: """
批量解码二维码 - 从 Excel/CSV/TXT 读取图片路径或 URL，本地批量解码并写回结果。

用法:
  python scripts/batch_decode.py --input <文件> [选项]

选项:
  --column <列名或索引>   指定 URL 列（Excel/CSV 有效），不指定时自动检测
  --output-txt <文件>     单独输出结果到 TXT（按行分隔）

默认行为: 在原文件中新增一列写入解码结果。
TXT 输入时结果默认写到 <原文件名>_decoded.txt。

输出 JSON:
  成功: {"total": N, "success": N, "failed": N, "output_file": "...", "output_txt": "..."|null}
  需要用户指定列: {"need_column": true, "columns": [...], "preview": [...]}
  错误: {"error": "..."}
"""

import sys
import json
import os
import csv
import argparse
import tempfile
from pathlib import Path

FAIL_PLACEHOLDER = "未解析到二维码"


def decode_single(image_source: str) -> str:
    """解码单个图片，返回内容文本或失败占位符。"""
    result = _try_zxing(image_source)
    return result if result else FAIL_PLACEHOLDER


def _is_url(s: str) -> bool:
    return s.startswith("http://") or s.startswith("https://")


def _try_zxing(source: str) -> str | None:
    try:
        import zxingcpp
        from PIL import Image
    except ImportError:
        return None

    tmp_path = None
    try:
        if _is_url(source):
            import urllib.request
            suffix = Path(source.split("?")[0]).suffix or ".png"
            tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
            urllib.request.urlretrieve(source, tmp.name)
            tmp.close()
            tmp_path = tmp.name
            img_path = tmp_path
        else:
            img_path = source

        img = Image.open(img_path)
        results = zxingcpp.read_barcodes(img)
        if results:
            return "; ".join(r.text for r in results)
        return None
    except Exception:
        return None
    finally:
        if tmp_path and os.path.exists(tmp_path):
            os.unlink(tmp_path)


# ── 文件读写 ──────────────────────────────────────────────

def process_txt(input_path: str, output_txt: str | None):
    with open(input_path, "r", encoding="utf-8") as f:
        lines = [line.strip() for line in f if line.strip()]

    results = []
    success = 0
    for line in lines:
        decoded = decode_single(line)
        results.append(decoded)
        if decoded != FAIL_PLACEHOLDER:
            success += 1

    out_path = output_txt or str(Path(input_path).with_stem(Path(input_path).stem + "_decoded").with_suffix(".txt"))
    with open(out_path, "w", encoding="utf-8") as f:
        f.write("\n".join(results))

    return {
        "total": len(lines),
        "success": success,
        "failed": len(lines) - success,
        "output_file": os.path.abspath(out_path),
        "output_txt": os.path.abspath(out_path),
    }


def process_csv(input_path: str, column, output_txt: str | None):
    with open(input_path, "r", encoding="utf-8-sig", newline="") as f:
        rows = list(csv.reader(f))
    if not rows:
        return {"error": "CSV 文件为空"}

    headers = rows[0]
    data_rows = rows[1:]
    col_idx = _resolve_col(headers, column)

    if col_idx is None:
        return {
            "need_column": True,
            "columns": headers,
            "preview": rows[:6],
            "message": "无法自动判断 URL 列，请指定 --column 参数" if column is None else f"找不到列 '{column}'",
        }

    decoded_results = []
    success = 0
    for row in data_rows:
        url = row[col_idx].strip() if col_idx < len(row) else ""
        if url:
            decoded = decode_single(url)
        else:
            decoded = FAIL_PLACEHOLDER
        decoded_results.append(decoded)
        if decoded != FAIL_PLACEHOLDER:
            success += 1

    if output_txt:
        with open(output_txt, "w", encoding="utf-8") as f:
            f.write("\n".join(decoded_results))
        return {
            "total": len(data_rows),
            "success": success,
            "failed": len(data_rows) - success,
            "output_file": os.path.abspath(input_path),
            "output_txt": os.path.abspath(output_txt),
        }

    result_col = "解码结果"
    headers.append(result_col)
    for i, row in enumerate(data_rows):
        row.append(decoded_results[i])

    with open(input_path, "w", encoding="utf-8-sig", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(headers)
        writer.writerows(data_rows)

    return {
        "total": len(data_rows),
        "success": success,
        "failed": len(data_rows) - success,
        "output_file": os.path.abspath(input_path),
        "output_txt": None,
    }


def process_excel(input_path: str, column, output_txt: str | None):
    import openpyxl

    wb = openpyxl.load_workbook(input_path)
    ws = wb.active

    headers = [str(c.value) if c.value is not None else "" for c in ws[1]]
    col_idx = _resolve_col(headers, column)

    if col_idx is None:
        preview = []
        for row in ws.iter_rows(min_row=1, max_row=min(6, ws.max_row), values_only=True):
            preview.append([str(c) if c is not None else "" for c in row])
        wb.close()
        return {
            "need_column": True,
            "columns": headers,
            "preview": preview,
            "message": "无法自动判断 URL 列，请指定 --column 参数" if column is None else f"找不到列 '{column}'",
        }

    decoded_results = []
    success = 0
    for row_idx in range(2, ws.max_row + 1):
        cell_val = ws.cell(row=row_idx, column=col_idx + 1).value
        url = str(cell_val).strip() if cell_val else ""
        if url:
            decoded = decode_single(url)
        else:
            decoded = FAIL_PLACEHOLDER
        decoded_results.append(decoded)
        if decoded != FAIL_PLACEHOLDER:
            success += 1

    if output_txt:
        with open(output_txt, "w", encoding="utf-8") as f:
            f.write("\n".join(decoded_results))
        wb.close()
        return {
            "total": len(decoded_results),
            "success": success,
            "failed": len(decoded_results) - success,
            "output_file": os.path.abspath(input_path),
            "output_txt": os.path.abspath(output_txt),
        }

    result_col_idx = ws.max_column + 1
    ws.cell(row=1, column=result_col_idx, value="解码结果")
    for i, decoded in enumerate(decoded_results):
        ws.cell(row=i + 2, column=result_col_idx, value=decoded)

    wb.save(input_path)
    wb.close()

    return {
        "total": len(decoded_results),
        "success": success,
        "failed": len(decoded_results) - success,
        "output_file": os.path.abspath(input_path),
        "output_txt": None,
    }


# ── 列检测 ──────────────────────────────────────────────

def _resolve_col(headers: list[str], column) -> int | None:
    if column is not None:
        try:
            idx = int(column)
            return idx if 0 <= idx < len(headers) else None
        except ValueError:
            pass
        col_lower = str(column).lower().strip()
        for i, h in enumerate(headers):
            if h.strip().lower() == col_lower:
                return i
        return None

    keywords = ["url", "link", "image", "img", "图片", "链接", "网址", "二维码"]
    for i, h in enumerate(headers):
        h_lower = h.strip().lower()
        for kw in keywords:
            if kw in h_lower:
                return i
    if len(headers) == 1:
        return 0
    return None


# ── 入口 ──────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", required=True, help="输入文件路径")
    parser.add_argument("--column", default=None, help="指定 URL 列（列名或索引）")
    parser.add_argument("--output-txt", default=None, help="单独输出结果到 TXT")
    args = parser.parse_args()

    input_path = args.input
    ext = Path(input_path).suffix.lower()

    if ext == ".txt":
        result = process_txt(input_path, args.output_txt)
    elif ext == ".csv":
        result = process_csv(input_path, args.column, args.output_txt)
    elif ext in (".xlsx", ".xls"):
        result = process_excel(input_path, args.column, args.output_txt)
    else:
        result = {"error": f"不支持的文件格式: {ext}，支持 txt/csv/xlsx"}

    print(json.dumps(result, ensure_ascii=False))
    if "error" in result:
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/decode.js
content: #!/usr/bin/env node
/**
 * QR Code Decoder - 本地 wechat-qr 解码 (Node.js 版)
 *
 * 用法:
 *   node scripts/decode.js <图片路径或URL>
 *   node scripts/decode.js --file <本地路径>
 *   node scripts/decode.js --url <图片URL>
 *
 * 输出 JSON:
 *   {"source": "wechat-qr", "contents": ["..."]}
 *   {"error": "..."}
 */

const https = require("https");
const http = require("http");
const fs = require("fs");
const os = require("os");
const path = require("path");

function output(source, contents) {
  console.log(JSON.stringify({ source, contents }));
  process.exit(0);
}

function error(msg) {
  console.log(JSON.stringify({ error: msg }));
  process.exit(1);
}

function isUrl(s) {
  return s.startsWith("http://") || s.startsWith("https://");
}

function downloadToTemp(url) {
  return new Promise((resolve, reject) => {
    const ext = path.extname(url.split("?")[0]) || ".png";
    const tmp = path.join(os.tmpdir(), `qr_${Date.now()}${ext}`);
    const mod = url.startsWith("https") ? https : http;
    const file = fs.createWriteStream(tmp);
    mod.get(url, (res) => {
      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
        file.close();
        fs.unlinkSync(tmp);
        return downloadToTemp(res.headers.location).then(resolve, reject);
      }
      res.pipe(file);
      file.on("finish", () => { file.close(); resolve(tmp); });
    }).on("error", (e) => { file.close(); if (fs.existsSync(tmp)) fs.unlinkSync(tmp); reject(e); });
  });
}

async function decodeWithWechat(imagePath) {
  try {
    const { scan } = await import("qr-scanner-wechat");
    const sharp = require("sharp");
    const { data, info } = await sharp(imagePath)
      .ensureAlpha()
      .raw()
      .toBuffer({ resolveWithObject: true });
    const result = await scan({
      data: Uint8ClampedArray.from(data),
      width: info.width,
      height: info.height,
    });
    return result?.text ? [result.text] : null;
  } catch {
    return null;
  }
}

async function main() {
  const argv = process.argv.slice(2);

  if (!argv.length) error("用法: node decode.js <图片路径或URL>");

  let mode, target;
  if ((argv[0] === "--file" || argv[0] === "--url") && argv.length >= 2) {
    mode = argv[0];
    target = argv[1];
  } else {
    target = argv[0];
    mode = isUrl(target) ? "--url" : "--file";
  }

  if (mode === "--file") {
    if (!fs.existsSync(target)) error(`文件不存在: ${target}`);

    const results = await decodeWithWechat(target);
    if (results) output("wechat-qr", results);

    error("无法解码: 本地 wechat-qr 未识别到二维码");
  } else {
    let tmpPath = null;
    try {
      tmpPath = await downloadToTemp(target);
      const results = await decodeWithWechat(tmpPath);
      if (results) output("wechat-qr", results);
    } catch (e) {
      error(`下载图片失败: ${e.message}`);
    } finally {
      if (tmpPath && fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
    }

    error("无法解码: 本地 wechat-qr 未识别到二维码");
  }
}

main();


path: scripts/decode.py
content: """
QR Code Decoder - 本地 zxing 解码。

用法:
  python scripts/decode.py <图片路径或URL>
  python scripts/decode.py --file <本地文件路径>
  python scripts/decode.py --url <图片URL>

输出 JSON:
  {"source": "zxing", "contents": ["..."]}
  错误时: {"error": "..."}
"""

import sys
import json
import os
import tempfile
from pathlib import Path


def is_url(s: str) -> bool:
    return s.startswith("http://") or s.startswith("https://")


def download_image(url: str) -> str:
    """下载图片到临时文件，返回临时文件路径。"""
    import urllib.request

    suffix = Path(url.split("?")[0]).suffix or ".png"
    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
    try:
        urllib.request.urlretrieve(url, tmp.name)
    except Exception as e:
        tmp.close()
        os.unlink(tmp.name)
        raise RuntimeError(f"下载图片失败: {e}")
    tmp.close()
    return tmp.name


def decode_with_zxing(image_path: str) -> list[str] | None:
    """使用 zxingcpp 本地解码，成功返回内容列表，失败返回 None。"""
    try:
        import zxingcpp
        from PIL import Image

        img = Image.open(image_path)
        results = zxingcpp.read_barcodes(img)
        if results:
            return [r.text for r in results]
        return None
    except ImportError:
        return None
    except Exception:
        return None


def output(source: str, contents: list[str]):
    print(json.dumps({"source": source, "contents": contents}, ensure_ascii=False))
    sys.exit(0)


def error(msg: str):
    print(json.dumps({"error": msg}, ensure_ascii=False))
    sys.exit(1)


def main():
    if len(sys.argv) < 2:
        error("用法: python decode.py <图片路径或URL>")

    args = sys.argv[1:]

    if not args:
        error("用法: python decode.py <图片路径或URL>")

    arg1 = args[0]
    if arg1 in ("--file", "--url") and len(args) >= 2:
        mode = arg1
        target = args[1]
    else:
        target = arg1
        mode = "--url" if is_url(target) else "--file"

    if mode == "--file":
        if not os.path.isfile(target):
            error(f"文件不存在: {target}")

        results = decode_with_zxing(target)
        if results:
            output("zxing", results)

        error("无法解码: 本地 zxing 未识别到二维码")

    else:  # --url
        tmp_path = None
        try:
            tmp_path = download_image(target)
            results = decode_with_zxing(tmp_path)
            if results:
                output("zxing", results)
        except RuntimeError as e:
            error(str(e))
        finally:
            if tmp_path and os.path.exists(tmp_path):
                os.unlink(tmp_path)

        error("无法解码: 本地 zxing 未识别到二维码")


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)