@puyinkai/xiaobao-cli 0.1.6 → 0.1.9
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/dist/{api-client-DWbwtQbl.mjs → api-client-CDNjkArS.mjs} +2 -2
- package/dist/{api-JZnABA4y.mjs → api-fsHHWWa3.mjs} +5 -4
- package/dist/{audio-BHWH6uhw.mjs → audio-CRSMqaKo.mjs} +2 -2
- package/dist/{auth-BpzWivxf.mjs → auth-_3hBh87U.mjs} +3 -3
- package/dist/cli.mjs +10 -10
- package/dist/{consultant-kV5r8Wj8.mjs → consultant-E33hTGUO.mjs} +1 -1
- package/dist/{customer-B2OV8BTR.mjs → customer-DcQ83oKD.mjs} +1 -1
- package/dist/{focus-BjWD5PRV.mjs → focus-XMRxnHPw.mjs} +1 -1
- package/dist/{list-C30zFN6h.mjs → list-B3nN4pd2.mjs} +1 -1
- package/dist/{list-CHububT5.mjs → list-CXquxzex.mjs} +1 -1
- package/dist/{list-FM3vkQJa.mjs → list-Cc4zcaNz.mjs} +1 -1
- package/dist/{list-VpZkzTEE.mjs → list-CnlQdzPp.mjs} +1 -1
- package/dist/{list-Bw8dFf8a.mjs → list-CrMOrnS-.mjs} +1 -1
- package/dist/{list-h1nuSJiN.mjs → list-DFdDcHFq.mjs} +1 -1
- package/dist/{list-CVMSGK08.mjs → list-dm9YaCG1.mjs} +1 -1
- package/dist/login-CUUcjWPd.mjs +156 -0
- package/dist/{logout-DVJX8_x6.mjs → logout-DP7REhCd.mjs} +2 -1
- package/dist/{project-CJkTgGOq.mjs → project-BD0g4oAW.mjs} +1 -1
- package/dist/{qa-CQDRFw-g.mjs → qa-CdMcxCE_.mjs} +1 -1
- package/dist/{resistance-Ss-eUZoN.mjs → resistance-6_V6At3T.mjs} +1 -1
- package/dist/{text-5q8lIWi8.mjs → text-C3Ap6L5n.mjs} +1 -1
- package/dist/{token-store-D2umpIk-.mjs → token-store-CXizLE2_.mjs} +37 -1
- package/dist/{visit-CUI2ov-H.mjs → visit-DoeECi8w.mjs} +1 -1
- package/dist/{whoami-Uv7GBtGB.mjs → whoami-z-76WaCP.mjs} +2 -2
- package/package.json +5 -1
- package/skills/wangxiaobao-audio-query/SKILL.md +2 -2
- package/skills/wangxiaobao-audio-wiki/SKILL.md +4 -4
- package/skills/wangxiaobao-customer-focus-query/SKILL.md +2 -2
- package/skills/wangxiaobao-customer-query/SKILL.md +2 -2
- package/skills/wangxiaobao-customer-resistance-query/SKILL.md +2 -2
- package/skills/wangxiaobao-html-to-pdf/SKILL.md +116 -0
- package/skills/wangxiaobao-html-to-pdf/scripts/html2pdf.py +132 -0
- package/skills/wangxiaobao-html-to-pdf/scripts/html2pdf_exact.py +92 -0
- package/skills/wangxiaobao-project-daily-report/SKILL.md +170 -0
- package/skills/wangxiaobao-project-daily-report/_meta.json +9 -0
- package/skills/wangxiaobao-project-daily-report/references/report_template.html +308 -0
- package/skills/wangxiaobao-project-daily-report/scripts/fetch_data.py +219 -0
- package/skills/wangxiaobao-project-daily-report/scripts/generate_report.py +1029 -0
- package/skills/wangxiaobao-project-daily-report/scripts/run.py +34 -0
- package/skills/wangxiaobao-project-weekly-report/SKILL.md +169 -0
- package/skills/wangxiaobao-project-weekly-report/scripts/generate_report.py +308 -0
- package/skills/wangxiaobao-project-weekly-report/scripts/html2pdf_fast.py +161 -0
- package/skills/wangxiaobao-project-weekly-report/scripts/logo_wangai.png +0 -0
- package/skills/wangxiaobao-project-weekly-report/scripts/logo_wangxiaobao.png +0 -0
- package/skills/wangxiaobao-project-weekly-report/scripts/prepare_data.py +682 -0
- package/skills/wangxiaobao-project-weekly-report/scripts/template.py +925 -0
- package/skills/wangxiaobao-quick-qa/SKILL.md +2 -2
- package/skills/wangxiaobao-shared/SKILL.md +40 -7
- package/skills/wangxiaobao-switch-project/SKILL.md +3 -3
- package/skills/wangxiaobao-visit-query/SKILL.md +2 -2
- package/dist/login-DtDxISE8.mjs +0 -91
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""一键生成项目日报(跨平台入口,替代旧的 Windows 专用 生成日报.bat)。
|
|
3
|
+
|
|
4
|
+
不指定日期时自动使用昨天的日期;其余参数原样透传给 generate_report.py。
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
python3 run.py # 用昨天的日期
|
|
8
|
+
python3 run.py 2026-05-19 # 指定日期
|
|
9
|
+
python3 run.py --project-name "示例项目" # 透传 generate_report.py 的参数
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import date, timedelta
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
19
|
+
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> int:
|
|
23
|
+
args = sys.argv[1:]
|
|
24
|
+
# 没有位置日期参数时,补上昨天的日期
|
|
25
|
+
if not any(DATE_RE.match(a) for a in args):
|
|
26
|
+
yesterday = (date.today() - timedelta(days=1)).isoformat()
|
|
27
|
+
args = [yesterday, *args]
|
|
28
|
+
print(f"未指定日期,使用昨天:{yesterday}")
|
|
29
|
+
cmd = [sys.executable, str(SCRIPT_DIR / "generate_report.py"), *args]
|
|
30
|
+
return subprocess.call(cmd)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
sys.exit(main())
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wangxiaobao-project-weekly-report
|
|
3
|
+
description: >
|
|
4
|
+
地产项目营销周报生成器。多数据源聚合(旺小宝API + IMA知识库 + SQL Agent),
|
|
5
|
+
自动拉取来访、成交、客户画像、顾问业绩等数据,生成8大模块结构化HTML周报,
|
|
6
|
+
含柱状图、圆环图、交叉分析表、专家深度洞察,同时生成HTML和PDF两种格式交付。
|
|
7
|
+
触发词:营销周报、项目周报、生成周报、weekly report、本周数据汇总、周度报告。
|
|
8
|
+
agent_created: true
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# 地产项目营销周报生成器(优化版)
|
|
12
|
+
|
|
13
|
+
## 触发条件
|
|
14
|
+
"生成周报"、"营销周报"、"项目周报"、"weekly report"、"本周数据汇总"、"周度报告"
|
|
15
|
+
|
|
16
|
+
## ⚡ 性能优化(2026-05更新)
|
|
17
|
+
|
|
18
|
+
| 模式 | 优化前 | 优化后 | 提升 |
|
|
19
|
+
|------|--------|--------|------|
|
|
20
|
+
| 完整(HTML+PDF) | ~10秒 | ~7秒 | **30%** |
|
|
21
|
+
| `--html-only`(仅HTML) | ~10秒 | **0.3秒** | **97%** |
|
|
22
|
+
|
|
23
|
+
**优化措施**:
|
|
24
|
+
1. 移除 `html2pdf.py` 中8秒强制等待(图表为静态SVG,无需JS渲染)
|
|
25
|
+
2. Chromium 启动参数优化(`--no-sandbox`, `--disable-gpu` 等)
|
|
26
|
+
3. 等待策略优化:`domcontentloaded` + 等待 `.card` 元素(最多2秒)
|
|
27
|
+
4. 新增 `--html-only` 模式:跳过PDF生成,0.3秒完成
|
|
28
|
+
|
|
29
|
+
## 输出
|
|
30
|
+
- **默认**:HTML + PDF 双格式(~7秒)
|
|
31
|
+
- **`--html-only`**:仅HTML(0.3秒),可用浏览器 Ctrl+P 打印为PDF
|
|
32
|
+
- 排版样式:深蓝渐变 header、卡片式布局、SVG图表(兼容打印)
|
|
33
|
+
|
|
34
|
+
通过 `preview_url` 预览 HTML + `deliver_attachments` 交付双文件。
|
|
35
|
+
|
|
36
|
+
## 执行流程(自动模式 · 推荐)
|
|
37
|
+
|
|
38
|
+
### 第1步:确认参数
|
|
39
|
+
从用户获取或自动推断:项目名、周日期范围。其他参数(tenant_id、project_id、KB_ID)从 `MEMORY.md` 获取。
|
|
40
|
+
|
|
41
|
+
### 第2步:拉取数据(prepare_data.py)
|
|
42
|
+
```bash
|
|
43
|
+
# 自动从以下来源拉取数据:
|
|
44
|
+
# 1. 旺小宝API(来访、客户、顾问)
|
|
45
|
+
# 2. IMA知识库(成交台账xlsx、盘客管理csv)
|
|
46
|
+
# 3. 本地JSON缓存(visit_lastweek_filtered.json等)
|
|
47
|
+
python3 prepare_data.py
|
|
48
|
+
```
|
|
49
|
+
输出:`weekly_report_data.json`(所有字段与 template.py 变量名完全匹配)
|
|
50
|
+
|
|
51
|
+
### 第3步:生成报告(generate_report.py)
|
|
52
|
+
```bash
|
|
53
|
+
# 默认:生成 HTML + PDF(~7秒)
|
|
54
|
+
python3 generate_report.py
|
|
55
|
+
|
|
56
|
+
# 极速模式:仅生成 HTML(0.3秒)
|
|
57
|
+
python3 generate_report.py --html-only
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 执行流程(手动模式 · 备用)
|
|
61
|
+
|
|
62
|
+
### 第1步:确认参数
|
|
63
|
+
同上。
|
|
64
|
+
|
|
65
|
+
### 第2步:拉取数据
|
|
66
|
+
同上(运行 `prepare_data.py`)。
|
|
67
|
+
|
|
68
|
+
### 第3步:修改 template.py(不推荐)
|
|
69
|
+
**已废弃**:当前版本通过 `generate_report.py` 自动覆盖变量,无需手动修改 `template.py` DATA区。
|
|
70
|
+
|
|
71
|
+
如果确需手动修改:
|
|
72
|
+
1. 将 `scripts/template.py` 复制到工作目录
|
|
73
|
+
2. 编辑 `template.py` 顶部 **DATA区** 的变量值
|
|
74
|
+
3. 运行:`python3 template.py`
|
|
75
|
+
4. 生成文件:`[项目名]_周报_[开始日期]-[结束日期].html` + `.pdf`
|
|
76
|
+
|
|
77
|
+
## 数据结构(weekly_report_data.json)
|
|
78
|
+
|
|
79
|
+
`prepare_data.py` 输出完整的 `weekly_report_data.json`,包含以下变量组:
|
|
80
|
+
|
|
81
|
+
| 变量组 | 内容 | 数据来源 |
|
|
82
|
+
|--------|------|----------|
|
|
83
|
+
| `project_name` | 项目全称 | 用户指定 |
|
|
84
|
+
| `brand_text` | header副标题 | 如"<项目名>项目 · 周度来访与成交深度分析" |
|
|
85
|
+
| `week_label` | 周标签 | 如"W20" |
|
|
86
|
+
| `date_start` / `date_end` | 周起始/结束日期 | 自动计算 |
|
|
87
|
+
| `data_date` | 数据截至日期 | 如"2026.05.18 23:30" |
|
|
88
|
+
| `week_days` | 本周7天来访数据 `[{"date":,"weekday":,"count":}]` | 旺小宝API |
|
|
89
|
+
| `last_week_days` | 上周7天来访数据 | 旺小宝API |
|
|
90
|
+
| `may_days` | 本月每日来访数据 | 旺小宝API |
|
|
91
|
+
| `consultants_this` | 顾问本周接待 `[{"name":,"count":,"pct":}]` | 旺小宝API |
|
|
92
|
+
| `first_visit` / `repeat_visit` | 首访/复访(基于visitCount字段) | 旺小宝API |
|
|
93
|
+
| `panke_done` / `panke_rate` | 盘客完成 | 旺小宝API |
|
|
94
|
+
| `intent_api` / `intent_panke` | 意向等级(双源) | 旺小宝API / 盘客CSV |
|
|
95
|
+
| `budgets` / `payments` / `downpay` | 预算/付款/首付 | 盘客CSV |
|
|
96
|
+
| `house_types` / `channels` / `resistances` / `purposes` | 户型/渠道/抗性/目的 | 盘客CSV |
|
|
97
|
+
| `focus_points` / `cycles` | 关注点/看房周期 | 盘客CSV |
|
|
98
|
+
| `deals_*` | 成交相关(total/amount/house_type/channel/cycle) | 成交台账xlsx |
|
|
99
|
+
| `visit_target` / `sales_target` | 月度目标 | 用户/配置 |
|
|
100
|
+
| `monthly_trend` / `monthly_trend_total` | 月度趋势 | 成交台账xlsx |
|
|
101
|
+
| `cross_cycle_intent` / `cross_purpose_intent` / `cross_channel_intent` | 交叉分析表 | 盘客CSV |
|
|
102
|
+
| `insight_xxx` | 8个模块的专家洞察文本 | AI根据数据撰写 |
|
|
103
|
+
| `suggestions` | 下周建议列表 | AI根据数据撰写 |
|
|
104
|
+
| `summary_text` | 总结文本 | AI根据数据撰写 |
|
|
105
|
+
|
|
106
|
+
## 专家洞察撰写要求
|
|
107
|
+
|
|
108
|
+
- **视角**:地产营销总监/操盘手视角
|
|
109
|
+
- **风格**:精炼有力、结论先行、数据支撑,80-150字/模块
|
|
110
|
+
- **禁止**:编造数据、过度乐观/悲观、堆砌专业术语
|
|
111
|
+
- **必须包含**:具体数字引用、可执行建议、风险预警
|
|
112
|
+
|
|
113
|
+
## 日期处理
|
|
114
|
+
- 使用 `datetime.weekday()` 计算,禁止硬编码星期
|
|
115
|
+
- 来访数据用 `visitTime` 字段做日期过滤
|
|
116
|
+
- 周报默认范围:上周一至上周日
|
|
117
|
+
- **首访/复访判断**:使用 `visitCount` 字段(=1首访,>1复访),不使用 `visitType` 字符串
|
|
118
|
+
|
|
119
|
+
## 使用方法
|
|
120
|
+
|
|
121
|
+
### AI 自动生成(推荐)
|
|
122
|
+
直接向 AI 说:**"生成 <项目名> 周报"**,AI 会自动:
|
|
123
|
+
1. 确认日期范围(或自动推断)
|
|
124
|
+
2. 运行 `prepare_data.py` 从旺小宝API + IMA知识库拉取最新数据
|
|
125
|
+
3. 根据数据生成专家洞察文本(`insight_xxx` 变量)
|
|
126
|
+
4. 运行 `generate_report.py` 渲染并输出 HTML + PDF
|
|
127
|
+
5. 预览 + 交付双文件
|
|
128
|
+
|
|
129
|
+
### 手动生成(备用)
|
|
130
|
+
1. 将 `scripts/template.py`、`scripts/generate_report.py`、`scripts/prepare_data.py` 复制到工作目录
|
|
131
|
+
2. 运行 `python3 prepare_data.py` 生成 `weekly_report_data.json`
|
|
132
|
+
3. 运行 `python3 generate_report.py` 生成 HTML + PDF
|
|
133
|
+
4. 用 `preview_url` 预览 HTML,或用 `deliver_attachments` 交付双文件
|
|
134
|
+
|
|
135
|
+
### 极速模式(--html-only)
|
|
136
|
+
```bash
|
|
137
|
+
python3 generate_report.py --html-only # 0.3秒生成HTML,无PDF
|
|
138
|
+
```
|
|
139
|
+
适用场景:需要调整模板样式、预览效果、或不需要PDF时。
|
|
140
|
+
|
|
141
|
+
## PDF生成说明
|
|
142
|
+
|
|
143
|
+
`generate_report.py` 调用 `html2pdf_fast.py`(优化版)将HTML转为PDF:
|
|
144
|
+
|
|
145
|
+
1. **依赖**:`playwright` + Chromium(需预先安装:`pip install playwright && playwright install chromium`)
|
|
146
|
+
2. **优化**:移除8秒强制等待,Chromium启动参数优化,~7秒完成
|
|
147
|
+
3. **质量**:与HTML排版样式完全一致(A4横向,保留背景色和CSS样式)
|
|
148
|
+
4. **降级**:如PDF生成失败,脚本会继续运行并输出错误信息,不会中断HTML生成
|
|
149
|
+
5. **极速替代**:使用 `--html-only` 跳过PDF,用浏览器Ctrl+P打印(用户侧控制)
|
|
150
|
+
|
|
151
|
+
## 注意事项
|
|
152
|
+
|
|
153
|
+
- Logo文件:`logo_wangai.png`、`logo_wangxiaobao.png` 需与生成的HTML同目录
|
|
154
|
+
- 若 `generate_report.py` 报错,检查Python版本(需3.8+)和 `weekly_report_data.json` 是否存在
|
|
155
|
+
- 洞察文本支持HTML标签(如 `<strong>`、`<span class="tag">`)
|
|
156
|
+
- 数据解读每次需根据新数据重新撰写,禁止直接复制上期文本
|
|
157
|
+
- **首访/复访字段**:已修复为 `visitCount` 判断(之前错误使用 `visitType` 字符串)
|
|
158
|
+
- **交叉表数据**:`cross_cycle_intent`/`cross_purpose_intent`/`cross_channel_intent` 由 `prepare_data.py` 自动生成,无需手动填写
|
|
159
|
+
|
|
160
|
+
## 文件清单(scripts/目录)
|
|
161
|
+
|
|
162
|
+
| 文件 | 用途 |
|
|
163
|
+
|------|------|
|
|
164
|
+
| `template.py` | HTML模板+CSS+图表生成函数+生成器 |
|
|
165
|
+
| `generate_report.py` | 报告生成器(读取JSON,覆盖变量,调用PDF) |
|
|
166
|
+
| `prepare_data.py` | 数据准备脚本(拉取API,解析xlsx/csv,输出JSON) |
|
|
167
|
+
| `html2pdf_fast.py` | 优化版HTML→PDF转换器(Playwright,无8秒等待) |
|
|
168
|
+
| `logo_wangai.png` | WangAI Logo(Header左侧) |
|
|
169
|
+
| `logo_wangxiaobao.png` | 旺小宝Logo(备用) |
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
营销周报生成器(完整版)
|
|
5
|
+
- 从 weekly_report_data.json 读取所有数据
|
|
6
|
+
- 覆盖 template.py 的 ALL 变量
|
|
7
|
+
- 调用 generate_html() + generate_pdf()
|
|
8
|
+
- 支持 --html-only:仅生成 HTML(<1秒)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys, os, json, re, argparse
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
|
|
14
|
+
# 解析命令行参数
|
|
15
|
+
parser = argparse.ArgumentParser(description="周报生成器")
|
|
16
|
+
parser.add_argument("--html-only", action="store_true", help="仅生成 HTML,跳过 PDF(<1秒)")
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
# 添加当前目录到 sys.path
|
|
20
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
21
|
+
|
|
22
|
+
print("=" * 60)
|
|
23
|
+
print("营销周报生成器(完整版)")
|
|
24
|
+
print("=" * 60)
|
|
25
|
+
|
|
26
|
+
# ── 1. 读取数据 ──
|
|
27
|
+
print("\n[1/5] 读取 weekly_report_data.json...")
|
|
28
|
+
with open('weekly_report_data.json', encoding='utf-8') as f:
|
|
29
|
+
data = json.load(f)
|
|
30
|
+
|
|
31
|
+
print(f" 项目: {data.get('project_name')}")
|
|
32
|
+
print(f" 周期: {data.get('date_start')} 至 {data.get('date_end')}")
|
|
33
|
+
week_total = sum(d['count'] for d in data.get('week_days', []))
|
|
34
|
+
print(f" 本周来访: {week_total} 组")
|
|
35
|
+
|
|
36
|
+
# ── 2. 导入 template 模块 ──
|
|
37
|
+
print("\n[2/5] 导入 template.py 模块...")
|
|
38
|
+
import template as tpl
|
|
39
|
+
print(f" ✅ 已导入 template 模块")
|
|
40
|
+
print(f" 当前 project_name: {tpl.project_name}")
|
|
41
|
+
|
|
42
|
+
# ── 3. 覆盖 ALL 变量 ──
|
|
43
|
+
print("\n[3/5] 覆盖 template.py 的所有变量...")
|
|
44
|
+
|
|
45
|
+
def convert_date(date_str):
|
|
46
|
+
"""YYYY-MM-DD → YYYY.MM.DD(用于 week_days 等)"""
|
|
47
|
+
if '-' in date_str and len(date_str) == 10:
|
|
48
|
+
parts = date_str.split('-')
|
|
49
|
+
return f"{parts[1]}.{parts[2]}"
|
|
50
|
+
return date_str
|
|
51
|
+
|
|
52
|
+
def convert_date_start(date_str):
|
|
53
|
+
"""YYYY-MM-DD → YYYY.MM.DD(用于 date_start/date_end)"""
|
|
54
|
+
if '-' in date_str and len(date_str) == 10:
|
|
55
|
+
return date_str.replace('-', '.')
|
|
56
|
+
return date_str
|
|
57
|
+
|
|
58
|
+
def convert_chart_data(raw):
|
|
59
|
+
"""
|
|
60
|
+
将 [{"name":..., "count":...}] 或 [[name, count, pct], ...] 统一转为 [(name, count, pct), ...]
|
|
61
|
+
template.py 的 hbar()/donut_svg() 需要此格式
|
|
62
|
+
"""
|
|
63
|
+
converted = []
|
|
64
|
+
for r in raw:
|
|
65
|
+
if isinstance(r, dict):
|
|
66
|
+
# JSON 中是 {"name": k, "count": v} 格式
|
|
67
|
+
name = r.get('name', '')
|
|
68
|
+
count = r.get('count', 0)
|
|
69
|
+
total = sum(x.get('count', 0) for x in raw)
|
|
70
|
+
pct = round(count / total * 100, 1) if total > 0 else 0
|
|
71
|
+
converted.append((name, count, pct))
|
|
72
|
+
elif isinstance(r, (list, tuple)) and len(r) >= 3:
|
|
73
|
+
# JSON 中是 [name, count, pct] 格式
|
|
74
|
+
converted.append((r[0], r[1], r[2]))
|
|
75
|
+
elif isinstance(r, (list, tuple)) and len(r) >= 2:
|
|
76
|
+
# [name, count] 格式,需要计算 pct
|
|
77
|
+
total = sum(x[1] for x in raw if isinstance(x, (list, tuple)) and len(x) >= 2)
|
|
78
|
+
pct = round(r[1] / total * 100, 1) if total > 0 else 0
|
|
79
|
+
converted.append((r[0], r[1], pct))
|
|
80
|
+
return converted
|
|
81
|
+
|
|
82
|
+
# 3.1 基本信息
|
|
83
|
+
tpl.project_name = data.get('project_name', tpl.project_name)
|
|
84
|
+
tpl.brand_text = data.get('brand_text', tpl.brand_text)
|
|
85
|
+
tpl.week_label = data.get('week_label', tpl.week_label)
|
|
86
|
+
tpl.date_start = convert_date_start(data.get('date_start', tpl.date_start))
|
|
87
|
+
tpl.date_end = convert_date_start(data.get('date_end', tpl.date_end))
|
|
88
|
+
data_date = data.get('data_date', tpl.data_date)
|
|
89
|
+
if '-' in data_date:
|
|
90
|
+
parts = data_date.split(' ')
|
|
91
|
+
if len(parts) == 2:
|
|
92
|
+
tpl.data_date = convert_date_start(parts[0]) + ' ' + parts[1]
|
|
93
|
+
else:
|
|
94
|
+
tpl.data_date = data_date
|
|
95
|
+
print(" ✅ 基本信息")
|
|
96
|
+
|
|
97
|
+
# 3.2 week_days / last_week_days / may_days
|
|
98
|
+
def build_week_days(days_list):
|
|
99
|
+
"""将 [{'date':..., 'weekday':..., 'count':...}] 转换为 (date_str, weekday, count) 元组列表"""
|
|
100
|
+
result = []
|
|
101
|
+
for d in days_list:
|
|
102
|
+
date_fmt = convert_date(d['date'])
|
|
103
|
+
result.append((date_fmt, d['weekday'], d['count']))
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
if 'week_days' in data:
|
|
107
|
+
tpl.week_days = build_week_days(data['week_days'])
|
|
108
|
+
print(f" ✅ week_days: {len(tpl.week_days)} 天")
|
|
109
|
+
|
|
110
|
+
if 'last_week_days' in data:
|
|
111
|
+
tpl.last_week_days = build_week_days(data['last_week_days'])
|
|
112
|
+
print(f" ✅ last_week_days: {len(tpl.last_week_days)} 天")
|
|
113
|
+
|
|
114
|
+
if 'may_days' in data:
|
|
115
|
+
tpl.may_days = build_week_days(data['may_days'])
|
|
116
|
+
print(f" ✅ may_days: {len(tpl.may_days)} 天")
|
|
117
|
+
|
|
118
|
+
# 3.3 顾问接待
|
|
119
|
+
if 'consultants_this' in data:
|
|
120
|
+
tpl.consultants_this = []
|
|
121
|
+
for c in data['consultants_this']:
|
|
122
|
+
tpl.consultants_this.append((c['name'], c['count'], c['pct'], c['last_count'], c['change_pct']))
|
|
123
|
+
print(f" ✅ consultants_this: {len(tpl.consultants_this)} 人")
|
|
124
|
+
|
|
125
|
+
# 3.4 首访/复访/盘客完成率
|
|
126
|
+
tpl.first_visit = data.get('first_visit', tpl.first_visit)
|
|
127
|
+
tpl.repeat_visit = data.get('repeat_visit', tpl.repeat_visit)
|
|
128
|
+
tpl.panke_done = data.get('panke_done', tpl.panke_done)
|
|
129
|
+
print(f" ✅ 首访={tpl.first_visit} 复访={tpl.repeat_visit} 盘客完成={tpl.panke_done}")
|
|
130
|
+
|
|
131
|
+
# 3.5 意向等级 API
|
|
132
|
+
if 'intent_api' in data:
|
|
133
|
+
tpl.intent_api = {}
|
|
134
|
+
for item in data['intent_api']:
|
|
135
|
+
tpl.intent_api[item['level']] = item['count']
|
|
136
|
+
print(f" ✅ intent_api: {tpl.intent_api}")
|
|
137
|
+
|
|
138
|
+
# 3.6 意向等级 盘客
|
|
139
|
+
if 'intent_panke' in data:
|
|
140
|
+
tpl.intent_panke = data['intent_panke']
|
|
141
|
+
print(f" ✅ intent_panke: {tpl.intent_panke}")
|
|
142
|
+
|
|
143
|
+
# 3.7 横向柱状图数据(预算/付款/户型/渠道/抗性/目的/关注点/周期/首付)
|
|
144
|
+
CHART_KEYS = [
|
|
145
|
+
('budgets', 'budgets'),
|
|
146
|
+
('payments', 'payments'),
|
|
147
|
+
('house_types', 'house_types'),
|
|
148
|
+
('channels', 'channels'),
|
|
149
|
+
('resistances', 'resistances'),
|
|
150
|
+
('purposes', 'purposes'),
|
|
151
|
+
('focus_points', 'focus_points'),
|
|
152
|
+
('cycles', 'cycles'),
|
|
153
|
+
('downpay', 'downpay'),
|
|
154
|
+
]
|
|
155
|
+
for json_key, tpl_var in CHART_KEYS:
|
|
156
|
+
if json_key in data:
|
|
157
|
+
# data[json_key] 是 [{"name":..., "count":...}], 或 [[name, count, pct], ...]
|
|
158
|
+
raw = data[json_key]
|
|
159
|
+
converted = []
|
|
160
|
+
for r in raw:
|
|
161
|
+
if isinstance(r, dict):
|
|
162
|
+
# JSON 中是 {"name": k, "count": v} 格式
|
|
163
|
+
name = r.get('name', '')
|
|
164
|
+
count = r.get('count', 0)
|
|
165
|
+
# 计算百分比
|
|
166
|
+
total = sum(x.get('count', 0) for x in raw) if all(isinstance(x, dict) for x in raw) else 1
|
|
167
|
+
pct = round(count / total * 100, 1) if total > 0 else 0
|
|
168
|
+
converted.append((name, count, pct))
|
|
169
|
+
elif isinstance(r, (list, tuple)) and len(r) >= 3:
|
|
170
|
+
# JSON 中是 [name, count, pct] 格式(元组转为了列表)
|
|
171
|
+
converted.append((r[0], r[1], r[2]))
|
|
172
|
+
elif isinstance(r, (list, tuple)) and len(r) >= 2:
|
|
173
|
+
# [name, count] 格式,需要计算 pct
|
|
174
|
+
total = sum(x[1] for x in raw if isinstance(x, (list, tuple)) and len(x) >= 2)
|
|
175
|
+
pct = round(r[1] / total * 100, 1) if total > 0 else 0
|
|
176
|
+
converted.append((r[0], r[1], pct))
|
|
177
|
+
setattr(tpl, tpl_var, converted)
|
|
178
|
+
print(f" ✅ {tpl_var}: {len(converted)} 项")
|
|
179
|
+
|
|
180
|
+
# 3.8 成交数据
|
|
181
|
+
tpl.deals_total = data.get('deals_total', tpl.deals_total)
|
|
182
|
+
tpl.deals_amount = data.get('deals_amount', tpl.deals_amount)
|
|
183
|
+
print(f" ✅ deals_total={tpl.deals_total} deals_amount={tpl.deals_amount}")
|
|
184
|
+
|
|
185
|
+
if 'house_type_deals' in data:
|
|
186
|
+
tpl.house_type_deals = convert_chart_data(data['house_type_deals'])
|
|
187
|
+
print(f" ✅ house_type_deals: {len(tpl.house_type_deals)} 项")
|
|
188
|
+
|
|
189
|
+
if 'channel_deals' in data:
|
|
190
|
+
tpl.channel_deals = convert_chart_data(data['channel_deals'])
|
|
191
|
+
print(f" ✅ channel_deals: {len(tpl.channel_deals)} 项")
|
|
192
|
+
|
|
193
|
+
if 'deal_cycle' in data:
|
|
194
|
+
tpl.deal_cycle = convert_chart_data(data['deal_cycle'])
|
|
195
|
+
print(f" ✅ deal_cycle: {len(tpl.deal_cycle)} 项")
|
|
196
|
+
|
|
197
|
+
# 3.9 月度目标
|
|
198
|
+
tpl.visit_target = data.get('visit_target', tpl.visit_target)
|
|
199
|
+
tpl.sales_target = data.get('sales_target', tpl.sales_target)
|
|
200
|
+
print(f" ✅ visit_target={tpl.visit_target} sales_target={tpl.sales_target}")
|
|
201
|
+
|
|
202
|
+
# 3.10 月度成交趋势
|
|
203
|
+
if 'monthly_trend' in data:
|
|
204
|
+
# data['monthly_trend'] 是 [{'month':..., 'count_2025':..., ...}]
|
|
205
|
+
# 需要转为 (month, count2025, amt2025, count2026, amt2026, yoy)
|
|
206
|
+
tpl.monthly_trend = []
|
|
207
|
+
for r in data['monthly_trend']:
|
|
208
|
+
tpl.monthly_trend.append((
|
|
209
|
+
r['month'],
|
|
210
|
+
r.get('count_2025', 0),
|
|
211
|
+
str(r.get('amount_2025', 0)),
|
|
212
|
+
r.get('count_2026', '待更新'),
|
|
213
|
+
str(r.get('amount', r.get('amount_2026', '待更新'))),
|
|
214
|
+
r.get('yoy', '—')
|
|
215
|
+
))
|
|
216
|
+
# monthly_trend_total
|
|
217
|
+
t2025 = sum(r.get('count_2025', 0) for r in data['monthly_trend'] if isinstance(r.get('count_2025'), int))
|
|
218
|
+
a2025 = sum(r.get('amount_2025', 0) for r in data['monthly_trend'] if isinstance(r.get('amount_2025'), (int,float)))
|
|
219
|
+
t2026 = sum(r.get('count_2026', 0) for r in data['monthly_trend'] if isinstance(r.get('count_2026'), int) and r.get('count_2026') != '待更新')
|
|
220
|
+
a2026 = sum(r.get('amount', r.get('amount_2026', 0)) for r in data['monthly_trend'] if isinstance(r.get('amount', r.get('amount_2026')), (int,float)) and r.get('amount', r.get('amount_2026')) != '待更新')
|
|
221
|
+
tpl.monthly_trend_total = ('合计/平均', t2025, f"{a2025:,}", t2026 if t2026 else '—', f"{a2026:,}" if a2026 else '—', '—')
|
|
222
|
+
print(f" ✅ monthly_trend: {len(tpl.monthly_trend)} 个月")
|
|
223
|
+
|
|
224
|
+
# 3.11 交叉分析表
|
|
225
|
+
def build_cross_table(json_key, default):
|
|
226
|
+
"""从 JSON 构建交叉表(列表的列表)"""
|
|
227
|
+
if json_key not in data:
|
|
228
|
+
return default
|
|
229
|
+
return data[json_key] # 已经是 [row_list, ...] 格式
|
|
230
|
+
|
|
231
|
+
if 'cross_cycle_intent' in data:
|
|
232
|
+
tpl.cross_cycle_intent = data['cross_cycle_intent']
|
|
233
|
+
print(f" ✅ cross_cycle_intent: {len(tpl.cross_cycle_intent)} 行")
|
|
234
|
+
|
|
235
|
+
if 'cross_purpose_intent' in data:
|
|
236
|
+
tpl.cross_purpose_intent = data['cross_purpose_intent']
|
|
237
|
+
print(f" ✅ cross_purpose_intent: {len(tpl.cross_purpose_intent)} 行")
|
|
238
|
+
|
|
239
|
+
if 'cross_channel_intent' in data:
|
|
240
|
+
tpl.cross_channel_intent = data['cross_channel_intent']
|
|
241
|
+
print(f" ✅ cross_channel_intent: {len(tpl.cross_channel_intent)} 行")
|
|
242
|
+
|
|
243
|
+
# 3.12 下周建议
|
|
244
|
+
if 'suggestions' in data:
|
|
245
|
+
tpl.suggestions = data['suggestions']
|
|
246
|
+
print(f" ✅ suggestions: {len(tpl.suggestions)} 条")
|
|
247
|
+
|
|
248
|
+
# 3.13 洞察文本(使用 JSON 中的值,若为空则保留 template 默认值)
|
|
249
|
+
INSIGHT_KEYS = [
|
|
250
|
+
'insight_core', 'insight_trend', 'insight_budget',
|
|
251
|
+
'insight_cycle', 'insight_purpose', 'insight_channel',
|
|
252
|
+
'insight_resistance', 'insight_consultant',
|
|
253
|
+
'insight_intent', 'insight_deals', 'insight_monthly',
|
|
254
|
+
'finding_intent', 'finding_deals', 'summary_text',
|
|
255
|
+
'risk_trend',
|
|
256
|
+
]
|
|
257
|
+
for key in INSIGHT_KEYS:
|
|
258
|
+
if key in data and data[key] and not data[key].startswith('首付') and not data[key].startswith('看房'):
|
|
259
|
+
# 有用数据,覆盖
|
|
260
|
+
# 注意:变量名在 template.py 中是 insight_core(无 _tpl 后缀)
|
|
261
|
+
# 但我们的 JSON key 也是 insight_core
|
|
262
|
+
# template.py 在 generate_html() 中用 fmt() 格式化 *_tpl 变量
|
|
263
|
+
# 所以我们需要同时设置 insight_core 和 insight_core_tpl
|
|
264
|
+
setattr(tpl, key, data[key])
|
|
265
|
+
# 同时设置 *_tpl 版本(用于格式化)
|
|
266
|
+
tpl_var_tpl = key + '_tpl'
|
|
267
|
+
if hasattr(tpl, tpl_var_tpl):
|
|
268
|
+
setattr(tpl, tpl_var_tpl, data[key])
|
|
269
|
+
print(f" ✅ {key}: {len(data[key])} 字")
|
|
270
|
+
elif key in data and data[key]:
|
|
271
|
+
setattr(tpl, key, data[key])
|
|
272
|
+
print(f" ✅ {key}: {len(data[key])} 字 (占位符)")
|
|
273
|
+
else:
|
|
274
|
+
print(f" ⚠️ {key}: 使用 template 默认值")
|
|
275
|
+
|
|
276
|
+
print(f"\n ✅ 所有变量覆盖完成!")
|
|
277
|
+
|
|
278
|
+
# ── 4. 生成 HTML ──
|
|
279
|
+
print("\n[4/4] 生成 HTML..." if args.html_only else "\n[4/5] 生成 HTML...")
|
|
280
|
+
html_content = tpl.generate_html()
|
|
281
|
+
print(f" ✅ HTML 已生成 ({len(html_content)} chars)")
|
|
282
|
+
|
|
283
|
+
# 写入 HTML 文件
|
|
284
|
+
output_file = f"{tpl.project_name.replace('·','').replace(' ', '_')}_周报_{tpl.date_start.replace('.','')}-{tpl.date_end.replace('.','')}.html"
|
|
285
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
286
|
+
f.write(html_content)
|
|
287
|
+
print(f" ✅ HTML 已写入: {output_file}")
|
|
288
|
+
|
|
289
|
+
# 生成 PDF(除非 --html-only)
|
|
290
|
+
if args.html_only:
|
|
291
|
+
print("\n⚡ --html-only 模式:跳过 PDF 生成")
|
|
292
|
+
pdf_path = None
|
|
293
|
+
else:
|
|
294
|
+
print("\n[5/5] 生成 PDF...")
|
|
295
|
+
pdf_path = tpl.generate_pdf(output_file)
|
|
296
|
+
if pdf_path:
|
|
297
|
+
print(f" ✅ PDF 已生成: {pdf_path}")
|
|
298
|
+
else:
|
|
299
|
+
print(f" ⚠️ PDF 生成失败(可能缺少 playwright 或 html2pdf.py)")
|
|
300
|
+
|
|
301
|
+
print("\n" + "=" * 60)
|
|
302
|
+
print("✅ 周报生成完成!")
|
|
303
|
+
print(f" HTML: {os.path.abspath(output_file)}")
|
|
304
|
+
if pdf_path:
|
|
305
|
+
print(f" PDF: {os.path.abspath(pdf_path)}")
|
|
306
|
+
else:
|
|
307
|
+
print(f" PDF: 未生成")
|
|
308
|
+
print("=" * 60)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""快*****版 HTML→PDF 转换器(Playwright headless Chromium)。
|
|
2
|
+
|
|
3
|
+
优化点(相比原版 html2pdf.py):
|
|
4
|
+
1. 移除 8 秒强制等待(wait_for_timeout 8000)
|
|
5
|
+
2. 改用 domcontentloaded(而非 networkidle),因为页面无外部 JS
|
|
6
|
+
3. 等待首张图片加载完成即触发 PDF 生成
|
|
7
|
+
4. 支持 --fast 参数跳过所有额外等待
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python html2pdf_fast.py report.html output.pdf --landscape
|
|
11
|
+
python html2pdf_fast.py report.html --fast # 极速模式
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import os
|
|
16
|
+
import argparse
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from playwright.sync_api import sync_playwright
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def html_to_pdf(
|
|
22
|
+
source: str,
|
|
23
|
+
pdf_path: str = None,
|
|
24
|
+
width: str = None,
|
|
25
|
+
height: str = None,
|
|
26
|
+
page_format: str = "A4",
|
|
27
|
+
landscape: bool = False,
|
|
28
|
+
fast: bool = False,
|
|
29
|
+
) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Convert HTML file or URL to PDF (optimized for static HTML + inline SVG).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
source: HTML 文件路径或 HTTP/HTTPS URL
|
|
35
|
+
pdf_path: 输出 PDF 路径(默认与 source 同名 .pdf)
|
|
36
|
+
width/height: 自定义页面尺寸(如 "210mm"、"1200px")
|
|
37
|
+
page_format: 页面格式(A4/A3/Letter…),width/height 设置后忽略
|
|
38
|
+
landscape: True → 横向(297×210mm)
|
|
39
|
+
fast: True → 跳过所有额外等待,domcontentloaded 后立即生成
|
|
40
|
+
"""
|
|
41
|
+
is_url = source.startswith("http://") or source.startswith("https://")
|
|
42
|
+
|
|
43
|
+
if not is_url:
|
|
44
|
+
source = os.path.abspath(source)
|
|
45
|
+
if not os.path.exists(source):
|
|
46
|
+
raise FileNotFoundError(f"HTML file not found: {source}")
|
|
47
|
+
file_url = Path(source).as_uri()
|
|
48
|
+
else:
|
|
49
|
+
file_url = source
|
|
50
|
+
|
|
51
|
+
if pdf_path is None:
|
|
52
|
+
if is_url:
|
|
53
|
+
from urllib.parse import urlparse
|
|
54
|
+
path_part = urlparse(source).path
|
|
55
|
+
base_name = Path(path_part).stem or "page"
|
|
56
|
+
pdf_path = os.path.join(os.getcwd(), base_name + ".pdf")
|
|
57
|
+
else:
|
|
58
|
+
pdf_path = source.rsplit(".", 1)[0] + ".pdf"
|
|
59
|
+
|
|
60
|
+
with sync_playwright() as p:
|
|
61
|
+
# 启动参数优化:加速 headless 模式
|
|
62
|
+
browser = p.chromium.launch(
|
|
63
|
+
headless=True,
|
|
64
|
+
args=[
|
|
65
|
+
'--no-sandbox',
|
|
66
|
+
'--disable-setuid-sandbox',
|
|
67
|
+
'--disable-dev-shm-usage',
|
|
68
|
+
'--disable-gpu',
|
|
69
|
+
'--single-process', # 单进程模式,启动更快(Windows 可能不支持)
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
page = browser.new_page()
|
|
73
|
+
|
|
74
|
+
# viewport 与 PDF 尺寸匹配,避免缩放
|
|
75
|
+
if width and height:
|
|
76
|
+
w_px = int(width.replace("px", "")) if "px" in width else 800
|
|
77
|
+
h_px = int(height.replace("px", "")) if "px" in height else 1100
|
|
78
|
+
page.set_viewport_size({"width": w_px, "height": h_px})
|
|
79
|
+
elif landscape:
|
|
80
|
+
page.set_viewport_size({"width": 1123, "height": 795}) # A4 横向 96dpi
|
|
81
|
+
else:
|
|
82
|
+
page.set_viewport_size({"width": 794, "height": 1123}) # A4 纵向 96dpi
|
|
83
|
+
|
|
84
|
+
# 强制 screen 媒体模式(确保按屏幕 CSS 渲染)
|
|
85
|
+
page.emulate_media(media="screen")
|
|
86
|
+
|
|
87
|
+
# 等待策略
|
|
88
|
+
if fast:
|
|
89
|
+
# 极速模式:仅等待 DOM 加载完成,不等待图片/字体
|
|
90
|
+
page.goto(file_url, wait_until="domcontentloaded")
|
|
91
|
+
else:
|
|
92
|
+
# 默认模式:等待页面稳定,确保图片/字体加载完成(保证质量)
|
|
93
|
+
page.goto(file_url, wait_until="domcontentloaded")
|
|
94
|
+
# 等待 header 渲染完成(确认页面框架就绪)
|
|
95
|
+
try:
|
|
96
|
+
page.wait_for_selector(".header", state="attached", timeout=3000)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
# 等待所有图片加载完成(确保 logo 等图片出现在 PDF 中)
|
|
100
|
+
try:
|
|
101
|
+
page.wait_for_function("""
|
|
102
|
+
() => Array.from(document.images).every(img => img.complete)
|
|
103
|
+
""", timeout=3000)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
# 构建 PDF 参数
|
|
108
|
+
pdf_params = {
|
|
109
|
+
"path": pdf_path,
|
|
110
|
+
"print_background": True,
|
|
111
|
+
"margin": {"top": "15mm", "bottom": "15mm", "left": "10mm", "right": "10mm"},
|
|
112
|
+
"display_header_footer": False,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if width and height:
|
|
116
|
+
pdf_params["width"] = width
|
|
117
|
+
pdf_params["height"] = height
|
|
118
|
+
elif landscape:
|
|
119
|
+
pdf_params["width"] = "297mm"
|
|
120
|
+
pdf_params["height"] = "210mm"
|
|
121
|
+
else:
|
|
122
|
+
pdf_params["format"] = page_format
|
|
123
|
+
|
|
124
|
+
page.pdf(**pdf_params)
|
|
125
|
+
browser.close()
|
|
126
|
+
|
|
127
|
+
return pdf_path
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
parser = argparse.ArgumentParser(
|
|
132
|
+
description="Fast HTML→PDF converter (Playwright, optimized for static HTML+SVG).",
|
|
133
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
134
|
+
epilog="""
|
|
135
|
+
Examples:
|
|
136
|
+
python html2pdf_fast.py report.html # A4 portrait, optimized wait
|
|
137
|
+
python html2pdf_fast.py report.html --landscape # A4 landscape
|
|
138
|
+
python html2pdf_fast.py report.html --fast # ultra-fast mode (no extra wait)
|
|
139
|
+
python html2pdf_fast.py report.html output.pdf --format A3 # A3 format
|
|
140
|
+
"""
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument("source", help="HTML file path or HTTP/HTTPS URL")
|
|
143
|
+
parser.add_argument("output", nargs="?", default=None, help="Output PDF path (default: same name as source)")
|
|
144
|
+
parser.add_argument("--width", help="Page width (e.g., '210mm', '1200px')")
|
|
145
|
+
parser.add_argument("--height", help="Page height (e.g., '297mm', '1697px')")
|
|
146
|
+
parser.add_argument("--format", default="A4", help="Page format: A4, A3, Letter, etc. (default: A4)")
|
|
147
|
+
parser.add_argument("--landscape", action="store_true", help="Use landscape orientation")
|
|
148
|
+
parser.add_argument("--fast", action="store_true", help="Ultra-fast mode: skip all extra waits")
|
|
149
|
+
|
|
150
|
+
args = parser.parse_args()
|
|
151
|
+
|
|
152
|
+
result = html_to_pdf(
|
|
153
|
+
source=args.source,
|
|
154
|
+
pdf_path=args.output,
|
|
155
|
+
width=args.width,
|
|
156
|
+
height=args.height,
|
|
157
|
+
page_format=args.format,
|
|
158
|
+
landscape=args.landscape,
|
|
159
|
+
fast=args.fast,
|
|
160
|
+
)
|
|
161
|
+
print(f"PDF generated: {result}")
|
|
Binary file
|