全案设计报价系统
本 skill 将工程量清单数据渲染为专业 PDF 报价单,支持封面、总价表、明细页多页输出。
1. 何时使用本 Skill
触发条件
以下场景应使用本 skill:
- 用户输入
/报价或/quote— 生成 PDF 报价单 - 用户提到要将多维表或 Excel 中的报价数据转为 PDF 报价单
以下场景不应使用本 skill:
- 用户只是在编辑多维表数据(应使用 lark-base skill)
- 用户只是在查看报价历史(不需要重新生成)
前置依赖
lark-baseskill — 读取飞书多维表数据lark-imskill — 发送 PDF 到飞书对话lark-driveskill — 上传 PDF 到飞书文档xlsxskill — 解析 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 文件时,不直接解析渲染。引导流程:
- 提示:推荐先导入飞书多维表,字段和顺序更好对齐
- 引导用户将 Excel 导入飞书:
- 打开飞书 → 新建多维表 → 导入 → 选择 Excel 文件
- 或者直接复制模板多维表,将 Excel 数据粘贴进去
- 用户提供多维表链接后,继续
/报价流程
2. 工作流程
用户触发 → 环境检查 → 确认数据源 → 读取数据 → 数据验证 → 确认 Logo → 确认税率 → 确认模板 → 渲染 PDF → 发送飞书
Step 1: 确认数据源
用户需提供以下之一:
- 飞书多维表链接 — 如
https://xxx.feishu.cn/base/XXX - Excel 文件 — 本地文件路径或通过对话上传
如果用户未提供,询问用户选择数据源。
Step 2: 读取数据
飞书多维表数据源
- 从用户提供的链接中提取
base-token(URL 中/base/后的部分) - 列出表:
lark-cli base +table-list --base-token <token> - 找到 项目信息 和 报价明细 两张表的 table_id
- 读取项目信息:
lark-cli base +record-list --base-token <token> --table-id <项目信息table_id> --format json - 先查视图:
lark-cli base +view-list --base-token <token> --table-id <报价明细table_id>,取默认 grid 视图(如「全部项目」)的 view_id - 读取报价明细(必须带
--view-id,否则返回顺序与用户在飞书界面拖拽后的顺序不一致):lark-cli base +record-list --base-token <token> --table-id <报价明细table_id> --view-id <视图ID> --format json --limit 200 - 从项目信息中提取:项目名称、工程编号、编制日期、编制人员、联系邮箱、公司Logo、税率、管理费
- 从报价明细中提取:区域、工程分类、项目名称、项目特征、单位、数量、综合单价、合价、备注(序号无需读取,渲染时自动生成)
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。
- 从多维表读取:检查项目信息表中的
公司Logo附件字段- 如有附件,使用 media API 下载(
drive +download不支持多维表附件):lark-cli api GET /open-apis/drive/v1/medias/{file_token}/download --output ./logo.png - 将图片转为 base64 data URI,传入渲染数据的
logo_url字段
- 如有附件,使用 media API 下载(
- 从 Excel 读取:检查 Excel 中是否有 logo 图片(通常嵌入在 sheet 中或作为附件)
- 如有,提取并转为 data URI
- Logo 缺失时(必须询问用户):
- 如果多维表或 Excel 中没有找到 Logo 图片,必须向用户询问:
- "未在数据源中找到公司 Logo,请问如何处理?"
- 选项 A:上传 Logo 图片
- 选项 B:提供 Logo 图片 URL
- 选项 C:不使用 Logo(PDF 中显示默认 "R M" 文字标识)
- 不要默认跳过这一步,即使用户说"直接生成"也要确认 Logo 处理方式
- 如果多维表或 Excel 中没有找到 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 Zebrabw— 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 areacategory— 按工程分类(默认),不传或加--group-by category
注意:选择"按区域"时,需确保报价明细表的
区域字段已填写(Step 3.7 会检查)。若数据尚未填写区域,建议优先使用"按工程分类"。
Step 5.7: 区域英文翻译(仅按区域分类时)
区域名每个项目不同,每次渲染前必须动态翻译,禁止套用写死的映射。
- 从报价明细中收集本次出现的全部区域名(去重)
- Agent 将区域名逐条翻译为英文(设计行业惯用翻译,如 玄关→Foyer、客厅→Living Room、阳光房→Sunroom)
- 将翻译结果作为
region_names对象写入渲染数据 JSON:
{
"region_names": {
"玄关": "Foyer",
"阳光房": "Sunroom"
}
}
- 渲染时 render.js 优先使用
region_names中的翻译(未提供的区域名会回退到内置常见区域映射,再兜底显示 AREA) - 渲染完成后在汇报中列出本次区域翻译对照表,用户可纠正,下次报价重新翻译
Step 6: 渲染 PDF
在项目目录下执行:
node scripts/render.js --input <data.json> --template <模板名> --vat-rate <税率> --group-by <area|category> --output ./output/<项目名称>_<工程编号>.pdf
--group-by area— 按区域分类(大分类=区域,明细含工程分类列)--group-by category或不传 — 按工程分类(原有行为)
渲染数据 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 — 发送到聊天窗口
lark-cli im +messages-send --type media --file <pdf路径>
选项 2 — 保存到飞书云文档
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 模板包含三种页面:
- 封面 — 工程名称、编号、日期、编制人员、logo(右上角)
- 总价表 — 按工程分类汇总金额 + 合计/增值税/总计
- 明细页 — 每页约 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/) |