# Quote Generator

> 工程量清单报价表生成：从飞书多维表或 Excel 文件读取报价数据，渲染为专业 PDF 报价单，自动发送到飞书对话并上传至飞书文档。当用户说 /报价、/quote 或提到生成报价单、报价表、工程量清单时使用。

- Skill: `pharder56k/quote-generator` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add pharder56k/quote-generator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pharder56k/quote-generator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: pharder56k (https://skillmd.com/u/pharder56k)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/pharder56k/quote-generator

---


# 全案设计报价系统

> 本 skill 将工程量清单数据渲染为专业 PDF 报价单，支持封面、总价表、明细页多页输出。

## 1. 何时使用本 Skill

### 触发条件

以下场景应使用本 skill：

- 用户输入 `/报价` 或 `/quote` — 生成 PDF 报价单
- 用户提到要将多维表或 Excel 中的报价数据转为 PDF 报价单

以下场景不应使用本 skill：

- 用户只是在编辑多维表数据（应使用 lark-base skill）
- 用户只是在查看报价历史（不需要重新生成）

### 前置依赖

- `lark-base` skill — 读取飞书多维表数据
- `lark-im` skill — 发送 PDF 到飞书对话
- `lark-drive` skill — 上传 PDF 到飞书文档
- `xlsx` skill — 解析 Excel 文件（当用户提供 Excel 时）
- Node.js 运行环境 + Playwright（已安装于项目目录）

### Step 0: 初次使用检查

> 当用户第一次使用 Skill，或执行命令时遇到依赖相关错误，Agent 应执行以下检查流程。

**1. 检查 Node.js 环境**

尝试执行 `node --version`，要求 >= 18。如果失败或版本过低：
- 提示用户安装 Node.js (>= 18): https://nodejs.org
- 建议使用 nvm 管理版本

**2. 检查 Playwright 浏览器**

检查项目目录下 `npx playwright install chromium` 是否已完成。如果 `render.js` 执行时报 `Executable doesn't exist` 错误：
- 在项目目录下执行: `npx playwright install chromium`
- 重试渲染

**3. 检查 lark-cli**

尝试执行 `lark-cli --version`。如果失败（command not found）：
- 提示用户安装 lark-cli（飞书命令行工具）
- 安装后执行: `lark-cli auth login`

**4. 检查飞书登录状态**

尝试执行 `lark-cli base +table-list --base-token LfjJbLrTHacijesHmIjcDtSpnJf` 测试登录状态。如果返回认证错误：
- 提示用户执行: `lark-cli auth login`
- 完成后重试

**5. 提供飞书模板**

用户需要把项目模板复制到自己的飞书空间：

- 模板链接: https://li1fn1sw90.feishu.cn/base/LfjJbLrTHacijesHmIjcDtSpnJf?from=from_copylink
- 引导用户点击链接 → 点击"复制此多维表" → 在自己的空间中填入项目数据和报价明细
- 完成后将新多维表链接发给 Agent

### Excel 数据源处理

用户提供 Excel 文件时，不直接解析渲染。引导流程：

1. 提示：推荐先导入飞书多维表，字段和顺序更好对齐
2. 引导用户将 Excel 导入飞书：
   - 打开飞书 → 新建多维表 → 导入 → 选择 Excel 文件
   - 或者直接复制模板多维表，将 Excel 数据粘贴进去
3. 用户提供多维表链接后，继续 `/报价` 流程

## 2. 工作流程

```
用户触发 → 环境检查 → 确认数据源 → 读取数据 → 数据验证 → 确认 Logo → 确认税率 → 确认模板 → 渲染 PDF → 发送飞书
```

### Step 1: 确认数据源

用户需提供以下之一：

1. **飞书多维表链接** — 如 `https://xxx.feishu.cn/base/XXX`
2. **Excel 文件** — 本地文件路径或通过对话上传

如果用户未提供，询问用户选择数据源。

### Step 2: 读取数据

#### 飞书多维表数据源

1. 从用户提供的链接中提取 `base-token`（URL 中 `/base/` 后的部分）
2. 列出表：`lark-cli base +table-list --base-token <token>`
3. 找到 **项目信息** 和 **报价明细** 两张表的 table_id
4. 读取项目信息：`lark-cli base +record-list --base-token <token> --table-id <项目信息table_id> --format json`
5. 先查视图：`lark-cli base +view-list --base-token <token> --table-id <报价明细table_id>`，取默认 grid 视图（如「全部项目」）的 view_id
6. 读取报价明细（**必须带 `--view-id`，否则返回顺序与用户在飞书界面拖拽后的顺序不一致**）：`lark-cli base +record-list --base-token <token> --table-id <报价明细table_id> --view-id <视图ID> --format json --limit 200`
7. 从项目信息中提取：项目名称、工程编号、编制日期、编制人员、联系邮箱、公司Logo、税率、管理费
8. 从报价明细中提取：区域、工程分类、项目名称、项目特征、单位、数量、综合单价、合价、备注（**序号无需读取，渲染时自动生成**）

#### Excel 数据源

使用 xlsx skill 解析 Excel，提取相同结构的数据。Excel 可能有多 sheet，需找到包含报价明细的 sheet（通常有"序号"、"工程分类"、"综合单价"等列头）。

### Step 3: 数据验证

> 在渲染 PDF 之前，Agent 必须对读取到的数据进行验证。不要直接跳过。

**验证清单（逐项执行）：**

**3.1 项目信息完整性**

检查 `项目名称`、`工程编号`、`编制日期` 三个字段：
- 任一为空 → 告知用户具体缺少哪个字段，请用户在飞书多维表的「项目信息」表中补充
- 全部通过 → 继续

**3.2 报价明细条目数**

检查 `items` 数组：
- 为 0 或不存在 → "报价明细中没有数据，请在飞书多维表的「报价明细」表中添加条目后重试"
- 有数据 → 继续

**3.3 逐条字段检查**

对每条记录检查必填字段（工程分类、项目名称、单位、数量、综合单价），汇总报告：

```
数据验证结果：
- 总条目: 104
- 区域缺失: 3 (第 6、9、20 条)
- 工程分类缺失: 2 (第 5、18 条)
- 项目名称缺失: 0
- 单位缺失: 1 (第 12 条)
- 数量缺失: 0
- 综合单价缺失: 3 (第 5、7、22 条)
- 工程分类不在标准列表中: 0
```

**3.4 缺失字段的处理**

- 缺失条目 ≤ 总条目的 20%：告知用户具体哪些条目有问题，询问"是否跳过这些条目继续生成？"
- 缺失条目 > 20%：要求用户修正数据后重试，不继续渲染
- 用户确认跳过 → 过滤掉问题条目，用有效数据继续

**3.7 区域缺失检查（仅按区域分类时）**

当用户选择按区域分类（Step 5.6 选择"按区域"）时，额外检查每条记录的 `区域` 字段：

```
区域缺失: X (第 X 条)
```

- 缺失条目 ≤ 总条目的 20%：告知用户具体哪些条目缺区域，询问"是否将这些条目归入『其他』继续生成？"
- 缺失条目 > 20%：要求用户补充区域后重试
- 用户确认 → 缺失区域的条目归入"其他"分组（与 Step 3.4 跳过逻辑互斥，用户二选一：跳过 or 归入其他）

**3.5 合价自动修正**

对每条记录检查 `合价` 是否等于 `数量 × 综合单价`：
- 偏差 < 1 元 → 不做处理
- 偏差 ≥ 1 元或为空 → 自动计算 `合价 = 数量 × 综合单价`，值写入渲染数据
- 修正条目超过 10% → 提示用户"已自动修正 X 条合价数据"，建议用户检查多维表公式

**3.6 工程分类标准列表**

以下为 13 类标准分类：

```
措施项目、拆除工程、砌筑工程、混凝土及钢筋混凝土工程、
金属结构工程、防水工程、保温隔热工程、楼地面装饰工程、
墙柱面装饰与隔断工程、天棚工程、油漆涂料工程、
其他装饰工程、安装工程
```

不在列表中的分类仍可渲染，但告知用户"分类 'XXX' 不在标准列表中"。

### Step 4: Logo 处理

> **必须执行，不可跳过。** 无论数据源是多维表还是 Excel，都必须检查并处理 Logo。

1. **从多维表读取**：检查项目信息表中的 `公司Logo` 附件字段
   - 如有附件，使用 **media API** 下载（`drive +download` 不支持多维表附件）：
     ```bash
     lark-cli api GET /open-apis/drive/v1/medias/{file_token}/download --output ./logo.png
     ```
   - 将图片转为 base64 data URI，传入渲染数据的 `logo_url` 字段
2. **从 Excel 读取**：检查 Excel 中是否有 logo 图片（通常嵌入在 sheet 中或作为附件）
   - 如有，提取并转为 data URI
3. **Logo 缺失时（必须询问用户）**：
   - 如果多维表或 Excel 中没有找到 Logo 图片，**必须向用户询问**：
     - "未在数据源中找到公司 Logo，请问如何处理？"
     - 选项 A：上传 Logo 图片
     - 选项 B：提供 Logo 图片 URL
     - 选项 C：不使用 Logo（PDF 中显示默认 "R M" 文字标识）
   - **不要默认跳过这一步**，即使用户说"直接生成"也要确认 Logo 处理方式

### Step 5: 确认税率与管理费

优先使用多维表项目信息中的 `税率`、`管理费` 字段值。如果多维表中没有该字段，则询问用户。

**费率规则（税率与管理费通用）：**
- 值 > 1 → 视为百分比整数，自动 ÷100（如 `3` → 3%、`10` → 10%）
- 值 ≤ 1 → 视为小数直接使用（如 `0.03` → 3%、`0.1` → 10%）
- 空值：税率默认 3%，管理费默认 0（不收取）

**计算顺序：**
```
合计 = Σ 各条目合价
管理费 = 合计 × 管理费率
增值税 = (合计 + 管理费) × 税率
总计 = 合计 + 管理费 + 增值税
```

> 管理费 > 0 时总价表显示「管理费」行；管理费 = 0 时不显示（兼容旧数据）。

### Step 5.5: 确认模板风格

> **必须询问，不可跳过。** 即使之前使用过某个模板，也要每次都确认。

**询问用户选择模板：**

```
请选择报价单模板风格：
1. Swiss IKB（默认）— 蓝底满版封面 + 双语分类标题
2. Swiss IKB Zebra — 同上 + 内容明细行斑马纹（白/浅蓝交替）
3. B&W — 白底封面 + 浅灰强调，适合黑白打印
4. B&W Zebra — 同上 + 内容明细行斑马纹（白/浅灰交替）
```

如果用户回复中包含明确的模板名称或编号，直接使用对应模板：
- `swiss-ikb` — Swiss IKB（默认）
- `swiss-ikb-zebra` — Swiss IKB Zebra
- `bw` — B&W 黑白打印版
- `bw-zebra` — B&W Zebra 黑白打印斑马纹版

> **开发中（暂不对外提供）：** Editorial / Editorial B&W / Card / Card B&W 四个模板代码已存在，待用户后续修改完善后启用。如用户主动要求使用这些模板，可执行 `node scripts/render.js --template editorial` 等命令，但优先推荐上述 4 个已上线模板。

### Step 5.6: 确认分类方式

> **必须询问，不可跳过。** 每次渲染前都要确认，不要默认使用上次的选项。

**询问用户选择分类方式：**

```
请选择报价单分类方式：
1. 按区域（玄关、客厅、主卧……）— 总价表按区域汇总，明细表在序号后增加工程分类列
2. 按工程分类（措施项目、拆除工程……）— 传统方式，明细表不含工程分类列
```

用户回复明确编号或名称后使用对应模式：
- `area` — 按区域分类，渲染命令加 `--group-by area`
- `category` — 按工程分类（默认），不传或加 `--group-by category`

> 注意：选择"按区域"时，需确保报价明细表的 `区域` 字段已填写（Step 3.7 会检查）。若数据尚未填写区域，建议优先使用"按工程分类"。

### Step 5.7: 区域英文翻译（仅按区域分类时）

> 区域名每个项目不同，**每次渲染前必须动态翻译，禁止套用写死的映射**。

1. 从报价明细中收集**本次出现的全部区域名**（去重）
2. Agent 将区域名逐条翻译为英文（设计行业惯用翻译，如 玄关→Foyer、客厅→Living Room、阳光房→Sunroom）
3. 将翻译结果作为 `region_names` 对象写入渲染数据 JSON：

```json
{
  "region_names": {
    "玄关": "Foyer",
    "阳光房": "Sunroom"
  }
}
```

4. 渲染时 render.js 优先使用 `region_names` 中的翻译（未提供的区域名会回退到内置常见区域映射，再兜底显示 AREA）
5. 渲染完成后在汇报中列出本次区域翻译对照表，用户可纠正，下次报价重新翻译

### Step 6: 渲染 PDF

在项目目录下执行：

```bash
node scripts/render.js --input <data.json> --template <模板名> --vat-rate <税率> --group-by <area|category> --output ./output/<项目名称>_<工程编号>.pdf
```

- `--group-by area` — 按区域分类（大分类=区域，明细含工程分类列）
- `--group-by category` 或不传 — 按工程分类（原有行为）

渲染数据 JSON 格式：

```json
{
  "项目名称": "xxx",
  "工程编号": "xxx",
  "编制日期": "xxx",
  "编制人员": "xxx",
  "联系邮箱": "xxx",
  "logo_url": "data:image/png;base64,...",
  "税率": 0.08,
  "管理费": 0.1,
  "items": [
    {
      "区域": "玄关",
      "工程分类": "措施项目",
      "项目名称": "脚手架",
      "项目特征": "室内脚手架",
      "单位": "项",
      "数量": 1,
      "综合单价": 3200,
      "合价": 3200
    }
  ]
}
```

> **序号自动生成规则**：`序号` 字段无需在数据中提供（飞书多维表可不建该列）。渲染时按 `分组序号.组内序号` 自动生成：按区域分类时 01 区域 → `1.1、1.2、…`，02 区域 → `2.1、2.2、…`；按工程分类时 措施项目 → `1.x`、拆除工程 → `2.x`…安装工程 → `13.x`。组内顺序 = 飞书记录顺序（多维表中可拖拽记录排序）。

**税率与管理费（详见 Step 5）：**
- `税率` / `管理费` 值 > 1 视为百分比整数（3 → 3%），≤ 1 视为小数（0.08 → 8%）
- 管理费 = 合计 × 管理费率；增值税 = (合计 + 管理费) × 税率；总计 = 合计 + 管理费 + 增值税
- 管理费未提供或为 0 → 不收取，总价表不显示管理费行
- 若未提供 `--vat-rate` 参数，render.js 优先读取数据中的 `税率` 字段

### Step 7: 保存与发送

PDF 渲染完成后，**必须询问用户**选择保存方式：

```
PDF 已生成：<项目名称>_<工程编号>_<模板名>.pdf（XX 条，¥XXX 含税）

请问如何保存？
1. 发送到当前聊天窗口
2. 保存到飞书云文档，推送文档链接
3. 两者都要
```

根据用户选择执行：

**选项 1 — 发送到聊天窗口**

```bash
lark-cli im +messages-send --type media --file <pdf路径>
```

**选项 2 — 保存到飞书云文档**

```bash
lark-cli drive +upload --file <pdf路径> --title "<项目名称>_<工程编号>"
```

获取上传后的文件链接，回复用户：

```
已保存到飞书云文档：<项目名称>_<工程编号>_<模板名>.pdf
文档链接: https://xxx.feishu.cn/drive/xxx
```

**选项 3 — 两者都要**

先执行选项 2（上传），再执行选项 1（发送文件 + 链接）。

**清理临时文件**

发送/保存完成后，删除渲染过程中产生的临时 JSON 文件和 Logo 下载文件（如有）。

## 3. 项目结构

```
quote-generator-skill/
├── package.json                # 项目配置
├── setup.sh                     # 一键安装脚本
├── SKILL.md                     # Skill 定义
├── scripts/
│   ├── render.js               # HTML → PDF 渲染引擎
│   ├── demo.js                 # 开箱即用演示
│   └── generate-large-test.js  # 大型测试数据生成器
├── references/
│   ├── templates/
│   │   ├── content.html        # Handlebars PDF 模板（内容页）
│   │   ├── cover-screen.html   # Swiss IKB 封面独立模板
│   │   ├── cover-bw.html       # B&W 黑白打印封面模板
│   │   ├── swiss-ikb.json      # Swiss IKB 配置
│   │   ├── swiss-ikb-zebra.json # Swiss IKB Zebra 配置
│   │   ├── bw.json             # B&W 黑白打印配置
│   │   └── bw-zebra.json       # B&W Zebra 配置
│   ├── helpers.js              # Handlebars 自定义 helper
│   └── bitable-config.json     # 多维表字段配置
├── docs/plans/                 # 设计文档
├── output/                     # 生成的 PDF 输出目录
└── README.md
```

## 4. 模板说明

PDF 模板包含三种页面：

1. **封面** — 工程名称、编号、日期、编制人员、logo（右上角）
2. **总价表** — 按工程分类汇总金额 + 合计/增值税/总计
3. **明细页** — 每页约 8 行数据，含分类标题、明细行、小计行、页码

## 5. 注意事项

- 多维表中的 `合价` 是公式字段（=数量×综合单价），读取时已计算好
- `序号` 无需在飞书中维护：渲染时自动生成（分组序号.组内序号，组内按记录顺序）。若要调整明细顺序，在飞书多维表中拖拽记录排序即可
- `工程分类` 是单选字段，13 个选项对应 13 类工程
- `区域` 是自由文本字段（建议在飞书中改为单选：玄关、客厅、餐厅、厨房、主卧、次卧、卫生间、阳台、书房、衣帽间、走廊、全屋等），仅按区域分类时使用，可为空
- 区域模式的分组标题显示中英对照（如 `Living Room 客厅`）。区域名每个项目不同，Agent 每次渲染前按 Step 5.7 动态翻译并写入 `region_names`；内置常见区域映射仅作兜底，未命中时显示 `AREA`
- `单位` 是单选字段，支持自定义扩展
- Logo 支持图片（推荐）和文字两种形式
- 增值税税率每次由用户指定，不固定
- 输出 PDF 为 A4 尺寸，适合打印

## 6. 常见问题处理

| 错误现象 | 原因 | Agent 处理方式 |
|----------|------|----------------|
| `command not found: lark-cli` | lark-cli 未安装 | 提示安装 lark-cli，参考 Step 0.3 |
| `lark-cli` 返回认证错误 | 未登录或 token 过期 | 提示执行 `lark-cli auth login` |
| `Executable doesn't exist` | Playwright Chromium 未安装 | 执行 `npx playwright install chromium` |
| 渲染 PDF 为空或格式混乱 | 数据 JSON 格式不正确 | 检查 `项目名称`、`工程编号` 非空，`items` 至少 1 条 |
| 明细序号与手动填的不一样 | 序号已改为自动生成 | 正常现象：序号按分组自动重编（如 01 全屋 → 1.1、1.2…）。调整顺序请在飞书拖拽记录 |
| 字号/字体异常 | 极少发生（字体已内置为 WOFF2，不依赖外部 CDN） | 检查项目 `references/fonts/` 目录下字体文件是否完整 |
| Logo 下载失败 | 多维表附件 API 权限问题 | 确认用户已授权，或选择"不使用 Logo" |
| `base-token` 无法从 URL 提取 | 用户提供的不是多维表链接 | 提示用户提供飞书多维表链接（URL 中应包含 `/base/`） |

