@puyinkai/xiaobao-cli 0.1.8 → 0.1.10

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.
Files changed (48) hide show
  1. package/dist/{api-client-zDBFgk0y.mjs → api-client-CDNjkArS.mjs} +1 -1
  2. package/dist/{api-CX99_4K7.mjs → api-fsHHWWa3.mjs} +1 -1
  3. package/dist/{audio-ShfvRomP.mjs → audio-CRSMqaKo.mjs} +2 -2
  4. package/dist/{auth-DpK81Vp5.mjs → auth-B4Yeoxae.mjs} +2 -2
  5. package/dist/cli.mjs +10 -10
  6. package/dist/{consultant-CS9B42d3.mjs → consultant-E33hTGUO.mjs} +1 -1
  7. package/dist/{customer-B1cxRbJ9.mjs → customer-DcQ83oKD.mjs} +1 -1
  8. package/dist/{focus-DfNPPhm5.mjs → focus-XMRxnHPw.mjs} +1 -1
  9. package/dist/{list-DI1OD95x.mjs → list-B3nN4pd2.mjs} +1 -1
  10. package/dist/{list-CD08BRLk.mjs → list-CXquxzex.mjs} +1 -1
  11. package/dist/{list-CKlx8YuL.mjs → list-Cc4zcaNz.mjs} +1 -1
  12. package/dist/{list-DCcCS6LM.mjs → list-CnlQdzPp.mjs} +1 -1
  13. package/dist/{list-D56gEqiB.mjs → list-CrMOrnS-.mjs} +1 -1
  14. package/dist/{list-CnlFIFiY.mjs → list-DFdDcHFq.mjs} +1 -1
  15. package/dist/{list-Cu6WzPpA.mjs → list-dm9YaCG1.mjs} +1 -1
  16. package/dist/{login-CUUcjWPd.mjs → login-EtQi30zd.mjs} +2 -1
  17. package/dist/{project-BuQQewOs.mjs → project-BD0g4oAW.mjs} +1 -1
  18. package/dist/{qa-CKsKiQIm.mjs → qa-CdMcxCE_.mjs} +1 -1
  19. package/dist/{resistance-DsE-Ha6Y.mjs → resistance-6_V6At3T.mjs} +1 -1
  20. package/dist/{text-C3jhdPoe.mjs → text-C3Ap6L5n.mjs} +1 -1
  21. package/dist/{visit-CX0Mc1n6.mjs → visit-DoeECi8w.mjs} +1 -1
  22. package/dist/{whoami-CZVez-rm.mjs → whoami-z-76WaCP.mjs} +1 -1
  23. package/package.json +5 -1
  24. package/skills/wangxiaobao-audio-query/SKILL.md +2 -2
  25. package/skills/wangxiaobao-audio-wiki/SKILL.md +4 -4
  26. package/skills/wangxiaobao-customer-focus-query/SKILL.md +2 -2
  27. package/skills/wangxiaobao-customer-query/SKILL.md +2 -2
  28. package/skills/wangxiaobao-customer-resistance-query/SKILL.md +2 -2
  29. package/skills/wangxiaobao-html-to-pdf/SKILL.md +116 -0
  30. package/skills/wangxiaobao-html-to-pdf/scripts/html2pdf.py +132 -0
  31. package/skills/wangxiaobao-html-to-pdf/scripts/html2pdf_exact.py +92 -0
  32. package/skills/wangxiaobao-project-daily-report/SKILL.md +170 -0
  33. package/skills/wangxiaobao-project-daily-report/_meta.json +9 -0
  34. package/skills/wangxiaobao-project-daily-report/references/report_template.html +308 -0
  35. package/skills/wangxiaobao-project-daily-report/scripts/fetch_data.py +219 -0
  36. package/skills/wangxiaobao-project-daily-report/scripts/generate_report.py +1029 -0
  37. package/skills/wangxiaobao-project-daily-report/scripts/run.py +34 -0
  38. package/skills/wangxiaobao-project-weekly-report/SKILL.md +169 -0
  39. package/skills/wangxiaobao-project-weekly-report/scripts/generate_report.py +308 -0
  40. package/skills/wangxiaobao-project-weekly-report/scripts/html2pdf_fast.py +161 -0
  41. package/skills/wangxiaobao-project-weekly-report/scripts/logo_wangai.png +0 -0
  42. package/skills/wangxiaobao-project-weekly-report/scripts/logo_wangxiaobao.png +0 -0
  43. package/skills/wangxiaobao-project-weekly-report/scripts/prepare_data.py +682 -0
  44. package/skills/wangxiaobao-project-weekly-report/scripts/template.py +925 -0
  45. package/skills/wangxiaobao-quick-qa/SKILL.md +2 -2
  46. package/skills/wangxiaobao-shared/SKILL.md +10 -8
  47. package/skills/wangxiaobao-switch-project/SKILL.md +3 -3
  48. package/skills/wangxiaobao-visit-query/SKILL.md +2 -2
@@ -0,0 +1,1029 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 旺小宝项目日报生成脚本
4
+ 功能:拉取旺小宝数据,生成完整HTML日报 + PDF
5
+
6
+ 使用方法:
7
+ python generate_report.py [日期]
8
+ python generate_report.py 2026-05-19
9
+ python generate_report.py 2026-05-19 --tenant-id <ID> --project-id <ID> --project-name "<项目名>"
10
+ """
11
+ import json
12
+ import os
13
+ import sys
14
+ import ssl
15
+ import argparse
16
+ import subprocess
17
+ from urllib.request import Request, urlopen
18
+ from datetime import datetime, timedelta
19
+ from collections import Counter
20
+
21
+ # 查找系统可用的浏览器(Chrome/Edge)路径
22
+ import shutil
23
+ _BROWSER_PATH = None
24
+ for _p in [
25
+ r"C:\Program Files\Google\Chrome\Application\chrome.exe",
26
+ r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
27
+ r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
28
+ r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
29
+ ]:
30
+ if os.path.exists(_p):
31
+ _BROWSER_PATH = _p
32
+ break
33
+ if _BROWSER_PATH is None:
34
+ _chrome = shutil.which("chrome")
35
+ _edge = shutil.which("msedge")
36
+ _BROWSER_PATH = _chrome or _edge
37
+
38
+ # ==================== 项目配置 ====================
39
+ # 优先级:命令行参数 > 环境变量 > 默认值
40
+ TENANT_ID = os.environ.get("XIAOBAO_TENANT_ID", "")
41
+ PROJECT_ID = os.environ.get("XIAOBAO_PROJECT_ID", "")
42
+ PROJECT_NAME = os.environ.get("XIAOBAO_PROJECT_NAME", "<项目名>")
43
+ TOKEN_PATH = os.path.join(os.path.expanduser("~"), ".openclaw/state/wangxiaobao/token.json")
44
+ OUTPUT_DIR = os.getcwd()
45
+
46
+ # KPI指标(示例值 500 / 21000,请按项目实际目标调整)
47
+ KPI_CONFIG = {
48
+ "month_visit_target": int(os.environ.get("XIAOBAO_MONTH_VISIT_TARGET", "500")), # 月度来访目标
49
+ "month_sales_target": int(os.environ.get("XIAOBAO_MONTH_SALES_TARGET", "21000")), # 月度销售目标(万元)
50
+ }
51
+
52
+
53
+ def get_token():
54
+ """获取旺小宝access token"""
55
+ with open(TOKEN_PATH, 'r', encoding='utf-8') as f:
56
+ return json.load(f)['access_token']
57
+
58
+
59
+ def api_call(endpoint, body, token):
60
+ """使用Python原生urllib调用旺小宝API,避免PowerShell编码问题"""
61
+ url = f"https://open-ai.wangxiaobao.com{endpoint}"
62
+ headers = {
63
+ "Authorization": f"Bearer {token}",
64
+ "X-Tenant-Id": TENANT_ID,
65
+ "X-Project-Id": PROJECT_ID,
66
+ "Content-Type": "application/json"
67
+ }
68
+ req = Request(url, data=body.encode('utf-8'), headers=headers, method='POST')
69
+ ctx = ssl.create_default_context()
70
+ try:
71
+ with urlopen(req, context=ctx, timeout=30) as resp:
72
+ data = resp.read().decode('utf-8')
73
+ return json.loads(data)
74
+ except Exception as e:
75
+ print(f" API调用失败: {e}")
76
+ return {'data': {'content': [], 'total': 0}}
77
+
78
+
79
+ def get_yesterday_visits(token, yesterday):
80
+ """获取昨日来访数据"""
81
+ body = json.dumps({
82
+ "current": 1, "size": 100,
83
+ "startTime": f"{yesterday} 00:00:00",
84
+ "endTime": f"{yesterday} 23:59:59"
85
+ }, ensure_ascii=False)
86
+ data = api_call("/ai-open/visits/page", body, token)
87
+ all_visits = data.get('data', {}).get('content', [])
88
+ return [v for v in all_visits if v.get('visitTime', '').startswith(yesterday)]
89
+
90
+
91
+ def get_month_visits(token, month_start, yesterday):
92
+ """获取本月来访数据"""
93
+ body = json.dumps({
94
+ "current": 1, "size": 500,
95
+ "startTime": f"{month_start} 00:00:00",
96
+ "endTime": f"{yesterday} 23:59:59"
97
+ }, ensure_ascii=False)
98
+ data = api_call("/ai-open/visits/page", body, token)
99
+ all_visits = data.get('data', {}).get('content', [])
100
+ month_prefix = yesterday[:7] # e.g., "2026-05"
101
+ return [v for v in all_visits if v.get('visitTime', '').startswith(month_prefix)]
102
+
103
+
104
+ def get_customers(token, month_start, yesterday):
105
+ """获取客户画像数据"""
106
+ body = json.dumps({
107
+ "page": 1, "size": 500,
108
+ "startTime": f"{month_start} 00:00:00",
109
+ "endTime": f"{yesterday} 23:59:59"
110
+ }, ensure_ascii=False)
111
+ data = api_call("/ai-open/customers/page", body, token)
112
+ return data.get('data', {}).get('content', [])
113
+
114
+
115
+ def get_stock_customers(token, three_months_ago, yesterday):
116
+ """获取存量客户(近3月未成交)"""
117
+ body = json.dumps({
118
+ "page": 1, "size": 100,
119
+ "startTime": f"{three_months_ago} 00:00:00",
120
+ "endTime": f"{yesterday} 23:59:59"
121
+ }, ensure_ascii=False)
122
+ data = api_call("/ai-open/customers/page", body, token)
123
+ all_custs = data.get('data', {}).get('content', [])
124
+ return [c for c in all_custs if c.get('dealStatus') != 1]
125
+
126
+
127
+ def analyze_intent(customers):
128
+ """分析意向等级分布"""
129
+ intent_dist = {"A级": 0, "B级": 0, "C级": 0, "D级": 0, "未知": 0}
130
+ for c in customers:
131
+ level = c.get('intentLevel', '未知') or '未知'
132
+ if level == '':
133
+ level = '未知'
134
+ # 映射
135
+ if level in ['A', 'A1', 'A2']:
136
+ intent_dist['A级'] += 1
137
+ elif level in ['B', 'B1', 'B2']:
138
+ intent_dist['B级'] += 1
139
+ elif level in ['C', 'C+', 'C1', 'C2']:
140
+ intent_dist['C级'] += 1
141
+ elif level in ['D', 'D1', 'D2']:
142
+ intent_dist['D级'] += 1
143
+ else:
144
+ intent_dist['未知'] += 1
145
+ return intent_dist
146
+
147
+
148
+ def analyze_channel(customers):
149
+ """分析渠道分布"""
150
+ channel_dist = Counter()
151
+ for c in customers:
152
+ tags = c.get('dynamicTags', {})
153
+ channel = tags.get('五大类来访渠道', '其他')
154
+ if not channel or channel == '':
155
+ channel = '其他'
156
+ channel_dist[channel] += 1
157
+ return dict(channel_dist)
158
+
159
+
160
+ def analyze_channel_with_intent(customers):
161
+ """分析渠道分布及意向等级"""
162
+ channel_data = {}
163
+ for c in customers:
164
+ tags = c.get('dynamicTags', {})
165
+ channel = tags.get('五大类来访渠道', '其他')
166
+ if not channel or channel == '':
167
+ channel = '其他'
168
+
169
+ if channel not in channel_data:
170
+ channel_data[channel] = {'total': 0, 'A级': 0, 'B级': 0, 'C级': 0, 'D级': 0}
171
+
172
+ channel_data[channel]['total'] += 1
173
+ level = c.get('intentLevel', '') or ''
174
+ if level in ['A', 'A1', 'A2']:
175
+ channel_data[channel]['A级'] += 1
176
+ elif level in ['B', 'B1', 'B2']:
177
+ channel_data[channel]['B级'] += 1
178
+ elif level in ['C', 'C+', 'C1', 'C2']:
179
+ channel_data[channel]['C级'] += 1
180
+ elif level in ['D', 'D1', 'D2']:
181
+ channel_data[channel]['D级'] += 1
182
+
183
+ return channel_data
184
+
185
+
186
+ def mask_name(name):
187
+ """脱敏客户姓名"""
188
+ if not name or len(name) < 2:
189
+ return "***"
190
+ return name[0] + '*' + name[-1] if len(name) > 1 else name[0] + '**'
191
+
192
+
193
+ def generate_html(report_data):
194
+ """生成HTML日报"""
195
+ today = report_data['date']
196
+ yesterday = report_data['yesterday']
197
+ month_start = report_data['month_start']
198
+ month_visit_target = report_data.get('month_visit_target', 500)
199
+ month_visit_actual = report_data['month_visit_actual']
200
+ month_sales_target = report_data.get('month_sales_target', 21000)
201
+ month_deals = report_data['month_deals']
202
+ deal_amount = report_data.get('deal_amount', '待填报')
203
+ yesterday_visits = report_data['yesterday_visits']
204
+ yesterday_visit_list = report_data.get('yesterday_visit_list', [])
205
+ intent_dist = report_data.get('intent_dist', {})
206
+ channel_dist = report_data.get('channel_dist', {})
207
+ stock_customers = report_data.get('stock_customers', [])
208
+ stock_by_channel = report_data.get('stock_by_channel', {})
209
+ deal_intent = report_data.get('deal_intent', {})
210
+
211
+ # 计算指标
212
+ days_in_month = 31
213
+ day_of_month = int(yesterday.split('-')[2])
214
+ time_progress = round(day_of_month / days_in_month * 100, 1)
215
+ visit_completion = round(month_visit_actual / month_visit_target * 100, 1)
216
+ progress_rate = round(visit_completion / time_progress * 100, 1) if time_progress > 0 else 0
217
+ surplus = round(progress_rate - 100, 1)
218
+
219
+ deal_amount_display = str(deal_amount) if deal_amount != '待填报' else '待填报'
220
+ sales_completion = round(int(deal_amount) / month_sales_target * 100, 1) if deal_amount != '待填报' else '-'
221
+ sales_progress_rate = round(sales_completion / time_progress * 100, 1) if deal_amount != '待填报' and time_progress > 0 else '-'
222
+
223
+ # 意向等级
224
+ intent_A = intent_dist.get('A级', 0)
225
+ intent_B = intent_dist.get('B级', 0)
226
+ intent_C = intent_dist.get('C级', 0)
227
+ intent_D = intent_dist.get('D级', 0)
228
+ intent_unk = intent_dist.get('未知', 0)
229
+ intent_total = intent_A + intent_B + intent_C + intent_D + intent_unk
230
+
231
+ intent_A_pct = round(intent_A / intent_total * 100, 1) if intent_total > 0 else 0
232
+ intent_B_pct = round(intent_B / intent_total * 100, 1) if intent_total > 0 else 0
233
+ intent_C_pct = round(intent_C / intent_total * 100, 1) if intent_total > 0 else 0
234
+ intent_D_pct = round(intent_D / intent_total * 100, 1) if intent_total > 0 else 0
235
+
236
+ # 成交意向
237
+ deal_A = deal_intent.get('A级', 0)
238
+ deal_B = deal_intent.get('B级', 0)
239
+ deal_C = deal_intent.get('C级', 0) + deal_intent.get('C/C+级', 0)
240
+ deal_D = deal_intent.get('D级', 0)
241
+
242
+ deal_A_pct = round(deal_A / month_deals * 100, 1) if month_deals > 0 else 0
243
+ deal_B_pct = round(deal_B / month_deals * 100, 1) if month_deals > 0 else 0
244
+ deal_C_pct = round(deal_C / month_deals * 100, 1) if month_deals > 0 else 0
245
+ deal_D_pct = round(deal_D / month_deals * 100, 1) if month_deals > 0 else 0
246
+
247
+ # 来访渠道行(使用实际渠道数据)
248
+ channel_rows = ""
249
+ channel_total = sum(channel_dist.values())
250
+ channel_notes = {
251
+ '中介': '主力获客渠道',
252
+ '自渠': '-',
253
+ '顺访': '-',
254
+ '中介,约访': '中介+约访',
255
+ '顺访,中介': '顺访+中介',
256
+ '约访': '-',
257
+ '自渠,顺访': '自渠+顺访',
258
+ '自渠,约访': '自渠+约访',
259
+ '全民/全员/老带新': '老带新',
260
+ }
261
+
262
+ # 渠道备注映射
263
+ channel_by_type = {
264
+ '中介': '主力获客渠道',
265
+ '自渠': '-',
266
+ '顺访': '-',
267
+ '约访': '-',
268
+ '全民/全员/老带新': '老带新渠道',
269
+ }
270
+
271
+ # 显示所有渠道
272
+ for ch, cnt in sorted(channel_dist.items(), key=lambda x: -x[1]):
273
+ pct = round(cnt / channel_total * 100, 1) if channel_total > 0 else 0
274
+ # 获取该渠道的备注
275
+ note = '-'
276
+ for key, val in channel_notes.items():
277
+ if key in ch:
278
+ note = val
279
+ break
280
+
281
+ channel_rows += f'''<tr>
282
+ <td><strong>{ch}</strong></td>
283
+ <td class="data-value">{cnt}</td>
284
+ <td>{pct}%</td>
285
+ <td>-</td>
286
+ <td>{note}</td>
287
+ </tr>
288
+ '''
289
+
290
+ # 存量客户汇总行
291
+ stock_summary_rows = ""
292
+ stock_channel_order = ['顺访+中介', '顺访', '中介', '自渠+顺访', '自渠', '其他']
293
+ stock_total = len(stock_customers)
294
+ for ch in stock_channel_order:
295
+ if ch in stock_by_channel:
296
+ d = stock_by_channel[ch]
297
+ pct = round(d['total'] / stock_total * 100, 1) if stock_total > 0 else 0
298
+ stock_summary_rows += f'''<tr>
299
+ <td><strong>{ch}</strong></td>
300
+ <td class="data-value">{d['total']}</td>
301
+ <td>{d['A级']}</td>
302
+ <td>{d['B级']}</td>
303
+ <td>{d['C级']}</td>
304
+ <td>{d['D级']}</td>
305
+ <td>{pct}%</td>
306
+ </tr>
307
+ '''
308
+ # 其他渠道
309
+ other_stock = sum(d['total'] for k, d in stock_by_channel.items() if k not in stock_channel_order)
310
+ if other_stock > 0:
311
+ stock_summary_rows += f'''<tr class="light-bg">
312
+ <td><strong>其他渠道</strong></td>
313
+ <td class="data-value">{other_stock}</td>
314
+ <td>-</td>
315
+ <td>-</td>
316
+ <td>-</td>
317
+ <td>-</td>
318
+ <td>-</td>
319
+ </tr>
320
+ '''
321
+ stock_summary_rows += f'''<tr style="background:#1F4E79; color:#fff;">
322
+ <td><strong>合计</strong></td>
323
+ <td class="data-value" style="color:#fff;">{stock_total}</td>
324
+ <td>-</td>
325
+ <td>-</td>
326
+ <td>-</td>
327
+ <td>-</td>
328
+ <td>100%</td>
329
+ </tr>
330
+ '''
331
+
332
+ # 存量客户明细
333
+ stock_detail_rows = ""
334
+ for i, c in enumerate(stock_customers[:10], 1):
335
+ level = c.get('intentLevel', 'C级') or 'C级'
336
+ level_display = f'<span class="data-value" style="color:#c00;">A级</span>' if level in ['A', 'A1', 'A2'] else f'{level}'
337
+ channel = c.get('dynamicTags', {}).get('五大类来访渠道', '-')
338
+ follow_status = '高意向' if level in ['A', 'A1', 'A2'] else '中等意向' if level in ['C', 'C+', 'C1', 'C2'] else '低意向'
339
+ decision_cycle = c.get('dynamicTags', {}).get('看房周期', '-') or '-'
340
+ no_deal_reason = c.get('dynamicTags', {}).get('购买抗性', '-') or '-'
341
+ visit_prob = 90 if level in ['A', 'A1', 'A2'] else 70 if level in ['C', 'C+', 'C1', 'C2'] else 40
342
+ deal_prob = 85 if level in ['A', 'A1', 'A2'] else 50 if level in ['C', 'C+', 'C1', 'C2'] else 20
343
+ strategy = '重点跟进,推动签约' if level in ['A', 'A1', 'A2'] else '保持联系' if level in ['D', 'D1', 'D2'] else '强化产品价值'
344
+
345
+ stock_detail_rows += f'''<tr>
346
+ <td class="no">{i}</td>
347
+ <td>{mask_name(c.get('customerName', ''))}</td>
348
+ <td>{channel}</td>
349
+ <td>{c.get('userName', '-')}</td>
350
+ <td>{c.get('lastVisitTime', '')[:10] if c.get('lastVisitTime') else '-'}</td>
351
+ <td>{level_display}</td>
352
+ <td>{follow_status}</td>
353
+ <td>{decision_cycle}</td>
354
+ <td>{no_deal_reason}</td>
355
+ <td>{visit_prob}%</td>
356
+ <td>{deal_prob}%</td>
357
+ <td>{strategy}</td>
358
+ </tr>
359
+ '''
360
+
361
+ # 昨日来访明细
362
+ yesterday_detail_rows = ""
363
+ first_visit_count = sum(1 for v in yesterday_visit_list if v['count'] == 1)
364
+ repeat_visit_count = len(yesterday_visit_list) - first_visit_count
365
+
366
+ for i, v in enumerate(yesterday_visit_list, 1):
367
+ visit_type = '首访' if v['count'] == 1 else '复访'
368
+ note = '新客户' if v['count'] == 1 else '-'
369
+ if v['count'] == 1 and v['duration'] > 60:
370
+ note = '高意向'
371
+
372
+ yesterday_detail_rows += f'''<tr>
373
+ <td class="no">{i}</td>
374
+ <td>{v['time']}</td>
375
+ <td>{v['advisor']}</td>
376
+ <td>第{v['count']}次</td>
377
+ <td>{v['duration']}分钟</td>
378
+ <td>{visit_type}</td>
379
+ <td>{note}</td>
380
+ </tr>
381
+ '''
382
+
383
+ yesterday_analysis = f"首访{first_visit_count}组({round(first_visit_count/len(yesterday_visit_list)*100,1) if yesterday_visit_list else 0}%" + \
384
+ f"),复访{repeat_visit_count}组({round(repeat_visit_count/len(yesterday_visit_list)*100,1) if yesterday_visit_list else 0}%)"
385
+
386
+ # 顾问来访统计
387
+ advisor_stats = Counter()
388
+ for v in yesterday_visit_list:
389
+ advisor_stats[v['advisor']] += 1
390
+ advisor_rows = ""
391
+ for i, (advisor, cnt) in enumerate(advisor_stats.most_common(5), 1):
392
+ remark = '存量客户多' if i == 1 else '-'
393
+ advisor_rows += f'''<tr>
394
+ <td>{i}</td>
395
+ <td><strong>{advisor}</strong></td>
396
+ <td>{cnt}组</td>
397
+ <td>-</td>
398
+ <td>{remark}</td>
399
+ </tr>
400
+ '''
401
+
402
+ # 成交金额警告
403
+ deal_warning = '<span style="color:#f90;">成交金额待确认,建议从销售管理系统补充数据</span>' if deal_amount == '待填报' else '-'
404
+ deal_amount_display = str(deal_amount) if deal_amount != '待填报' else '待填报'
405
+
406
+ # 来访指标说明
407
+ visit_comment = '大幅超额完成' if progress_rate > 130 else '超额完成' if progress_rate > 100 else '按计划推进'
408
+
409
+ # 昨日来访评论
410
+ yesterday_comment = '平稳' if 8 <= yesterday_visits <= 15 else '较好' if yesterday_visits > 15 else '较少'
411
+
412
+ # ========== 方案A:纯CSS方案 =========
413
+ # 不依赖 print-color-adjust,改用深色文字+边框,保证PDF可见
414
+ # 表头背景用内联 style 强制写入,双重保险
415
+
416
+ # 组装HTML
417
+ html = f'''<!DOCTYPE html>
418
+ <html lang="zh-CN">
419
+ <head>
420
+ <meta charset="UTF-8">
421
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
422
+ <title>{PROJECT_NAME} 日报 {yesterday}</title>
423
+ <style>
424
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
425
+ body {{ font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif; font-size: 12px; color: #333; background: #fff; padding: 15px; }}
426
+ table {{ width: 100%; border-collapse: collapse; margin-bottom: 12px; }}
427
+ th, td {{ border: 1px solid #ddd; padding: 6px 8px; text-align: center; vertical-align: middle; font-size: 11px; }}
428
+ th {{ color: #fff; font-weight: bold; }}
429
+
430
+ /* 颜色 */
431
+ .title-bg {{ background: #1F4E79 !important; }}
432
+ .orange-bg {{ background: #975765 !important; }}
433
+ .pink-bg {{ background: #EEE2E6 !important; color: #333; }}
434
+ .peach-bg {{ background: #C59BA6 !important; }}
435
+ .light-bg {{ background: #f5f5f5 !important; }}
436
+
437
+ /* KPI卡片 */
438
+ .kpi-row {{ display: flex; gap: 8px; margin-bottom: 12px; }}
439
+ .kpi-card {{ flex: 1; border: 2px solid #1F4E79; border-radius: 4px; padding: 10px; text-align: center; background: #fafafa; }}
440
+ .kpi-card .label {{ font-size: 10px; color: #666; margin-bottom: 3px; }}
441
+ .kpi-card .value {{ font-size: 22px; font-weight: bold; color: #1F4E79; line-height: 1.2; }}
442
+ .kpi-card .sub {{ font-size: 9px; color: #888; margin-top: 3px; }}
443
+
444
+ /* 布局 */
445
+ .col-2 {{ display: flex; gap: 12px; margin-bottom: 12px; }}
446
+ .col-2 > div {{ flex: 1; }}
447
+
448
+ /* 表头 */
449
+ .section-title {{ background: #1F4E79 !important; color: #fff !important; font-size: 13px; font-weight: bold; padding: 8px; text-align: center; }}
450
+ .table-header {{ background: #975765 !important; color: #fff !important; font-weight: bold; }}
451
+ .table-header-pink {{ background: #EEE2E6 !important; color: #333 !important; font-weight: bold; }}
452
+ .table-header-peach {{ background: #C59BA6 !important; color: #fff !important; font-weight: bold; }}
453
+
454
+ /* 数据 */
455
+ .data-value {{ font-weight: bold; font-size: 14px; color: #1F4E79; }}
456
+ .data-red {{ color: #c00; font-weight: bold; }}
457
+ .highlight {{ background: #fff3cd; }}
458
+
459
+ /* 序号列 */
460
+ .no {{ width: 30px; }}
461
+
462
+ /* 明细表格 */
463
+ .detail-table td {{ text-align: left; font-size: 10px; }}
464
+ .detail-table td:nth-child(2) {{ text-align: center; }}
465
+ .detail-table td:nth-child(3) {{ text-align: center; }}
466
+ .detail-table td:nth-child(4) {{ text-align: center; }}
467
+ .detail-table td:nth-child(5) {{ text-align: center; }}
468
+ .detail-table td:nth-child(6) {{ text-align: center; }}
469
+
470
+ /* 交替行 */
471
+ tr:nth-child(even) {{ background: #f9f9f9; }}
472
+
473
+ /* 页脚 */
474
+ .footer {{ text-align: center; padding: 15px; color: #999; font-size: 10px; border-top: 1px solid #ddd; margin-top: 20px; }}
475
+
476
+ /* 渠道表 */
477
+ .channel-table td:first-child {{ text-align: left; font-weight: bold; }}
478
+
479
+ /* 合并标题 */
480
+ .merge-cell {{ background: #975765 !important; color: #fff !important; font-weight: bold; }}
481
+
482
+ /* 打印优化:强制背景色打印 + 文字深色fallback */
483
+ @media print {{
484
+ @page {{ margin: 8mm; size: A4; }}
485
+ body {{
486
+ -webkit-print-color-adjust: exact !important;
487
+ print-color-adjust: exact !important;
488
+ background: #fff !important;
489
+ }}
490
+ table {{ break-inside: avoid; }}
491
+ tr {{ break-inside: avoid; }}
492
+ .kpi-row {{ display: flex !important; flex-wrap: nowrap; break-inside: avoid; }}
493
+ .kpi-row > div {{ flex: 1; min-width: 0; }}
494
+ .col-2 {{ display: block !important; break-inside: avoid; }}
495
+ .col-2 > div {{ display: block; width: 100% !important; margin-bottom: 8px; }}
496
+ .footer {{ break-inside: avoid; }}
497
+
498
+ /* 强制所有深色背景元素的背景色打印 */
499
+ .section-title, .title-bg {{
500
+ background: #1F4E79 !important;
501
+ color: #fff !important;
502
+ -webkit-print-color-adjust: exact !important;
503
+ print-color-adjust: exact !important;
504
+ }}
505
+ .table-header td, .orange-bg, .merge-cell, th {{
506
+ background: #975765 !important;
507
+ color: #fff !important;
508
+ -webkit-print-color-adjust: exact !important;
509
+ print-color-adjust: exact !important;
510
+ }}
511
+ .table-header {{ background: #975765 !important; }}
512
+ .table-header-peach, .peach-bg {{
513
+ background: #C59BA6 !important;
514
+ color: #fff !important;
515
+ -webkit-print-color-adjust: exact !important;
516
+ print-color-adjust: exact !important;
517
+ }}
518
+ .table-header-pink, .pink-bg {{
519
+ background: #EEE2E6 !important;
520
+ color: #333 !important;
521
+ -webkit-print-color-adjust: exact !important;
522
+ print-color-adjust: exact !important;
523
+ }}
524
+ .light-bg {{
525
+ background: #f5f5f5 !important;
526
+ color: #333 !important;
527
+ -webkit-print-color-adjust: exact !important;
528
+ print-color-adjust: exact !important;
529
+ }}
530
+ }}
531
+ </style>
532
+ </head>
533
+ <body>
534
+
535
+ <!-- 项目标题 -->
536
+ <table>
537
+ <tr>
538
+ <td class="title-bg" style="font-size: 18px; font-weight: bold; padding: 12px; text-align: center; color: #fff;">
539
+ {PROJECT_NAME} 营销日报
540
+ </td>
541
+ </tr>
542
+ <tr>
543
+ <td style="text-align: center; padding: 6px; color: #666; background: #f5f5f5; font-size: 11px;">
544
+ 数据日期:{yesterday}(昨日) | 统计周期:{month_start} ~ {yesterday} | 截至时间:23:59
545
+ </td>
546
+ </tr>
547
+ </table>
548
+
549
+ <!-- KPI概览 -->
550
+ <div class="kpi-row">
551
+ <div class="kpi-card">
552
+ <div class="label">本月来访(组)</div>
553
+ <div class="value">{month_visit_actual}</div>
554
+ <div class="sub">目标{month_visit_target}组 | 完成率{visit_completion}%</div>
555
+ </div>
556
+ <div class="kpi-card">
557
+ <div class="label">来访进度达成</div>
558
+ <div class="value">{progress_rate}%</div>
559
+ <div class="sub">时间进度{time_progress}% | 超额{surplus}%</div>
560
+ </div>
561
+ <div class="kpi-card">
562
+ <div class="label">本月成交(组)</div>
563
+ <div class="value">{month_deals}</div>
564
+ <div class="sub">目标待确认 | 成交金额{deal_amount}</div>
565
+ </div>
566
+ <div class="kpi-card">
567
+ <div class="label">昨日来访(组)</div>
568
+ <div class="value">{yesterday_visits}</div>
569
+ <div class="sub">本周来访量{yesterday_comment}</div>
570
+ </div>
571
+ </div>
572
+
573
+ <!-- 来访指标 + 来访意向等级 -->
574
+ <div class="col-2">
575
+ <div>
576
+ <table>
577
+ <tr><td colspan="6" class="section-title" style="font-size:12px;">本月来访指标及达成</td></tr>
578
+ <tr class="table-header">
579
+ <td>月度客储指标</td>
580
+ <td>截至昨日来访</td>
581
+ <td>完成率</td>
582
+ <td>进度达成率</td>
583
+ <td>总组数</td>
584
+ <td>转化率</td>
585
+ </tr>
586
+ <tr>
587
+ <td class="data-value">{month_visit_target}</td>
588
+ <td class="data-value">{month_visit_actual}</td>
589
+ <td class="data-red">{visit_completion}%</td>
590
+ <td class="data-red">{progress_rate}%</td>
591
+ <td class="data-value">{month_visit_actual}</td>
592
+ <td>-</td>
593
+ </tr>
594
+ <tr style="background:#f5f5f5; font-size:9px;">
595
+ <td colspan="6" style="text-align:left; color:#888;">注:时间进度{time_progress}%({day_of_month}/{days_in_month}天),来访指标{visit_comment}</td>
596
+ </tr>
597
+ </table>
598
+ </div>
599
+
600
+ <div>
601
+ <table>
602
+ <tr><td colspan="5" class="section-title" style="font-size:12px;">本月来访客户意向等级分析</td></tr>
603
+ <tr class="table-header">
604
+ <td>A级(高意向)</td>
605
+ <td>B级(中高)</td>
606
+ <td>C级(中意向)</td>
607
+ <td>D级(低意向)</td>
608
+ <td>合计</td>
609
+ </tr>
610
+ <tr>
611
+ <td class="data-value">{intent_A}</td>
612
+ <td class="data-value">{intent_B}</td>
613
+ <td class="data-value">{intent_C}</td>
614
+ <td class="data-value">{intent_D}</td>
615
+ <td class="data-value" style="font-size:16px;">{intent_total}</td>
616
+ </tr>
617
+ <tr style="background:#f5f5f5;">
618
+ <td>{intent_A_pct}%</td>
619
+ <td>{intent_B_pct}%</td>
620
+ <td>{intent_C_pct}%</td>
621
+ <td>{intent_D_pct}%</td>
622
+ <td>100%</td>
623
+ </tr>
624
+ </table>
625
+ </div>
626
+ </div>
627
+
628
+ <!-- 销售指标 + 成交意向等级 -->
629
+ <div class="col-2">
630
+ <div>
631
+ <table>
632
+ <tr><td colspan="6" class="section-title" style="font-size:12px;">本月销售指标及达成</td></tr>
633
+ <tr class="table-header">
634
+ <td>月度销售指标(万)</td>
635
+ <td>截至昨日成交(万)</td>
636
+ <td>完成率</td>
637
+ <td>进度达成率</td>
638
+ <td>总套数</td>
639
+ <td>转化率</td>
640
+ </tr>
641
+ <tr>
642
+ <td class="data-value">{month_sales_target}</td>
643
+ <td class="data-value">{deal_amount_display}</td>
644
+ <td class="data-red">{sales_completion}</td>
645
+ <td class="data-red">{sales_progress_rate}</td>
646
+ <td class="data-value">{month_deals}</td>
647
+ <td>-</td>
648
+ </tr>
649
+ <tr style="background:#fff3cd; font-size:9px;">
650
+ <td colspan="6" style="text-align:left;">{deal_warning}</td>
651
+ </tr>
652
+ </table>
653
+ </div>
654
+
655
+ <div>
656
+ <table>
657
+ <tr><td colspan="5" class="section-title" style="font-size:12px;">本月成交客户意向等级分析</td></tr>
658
+ <tr class="table-header">
659
+ <td>A级</td>
660
+ <td>B级</td>
661
+ <td>C/C+级</td>
662
+ <td>D级</td>
663
+ <td>合计</td>
664
+ </tr>
665
+ <tr>
666
+ <td class="data-value">{deal_A}</td>
667
+ <td>{deal_B if deal_B else '-'}</td>
668
+ <td>{deal_C if deal_C else '-'}</td>
669
+ <td>{deal_D if deal_D else '-'}</td>
670
+ <td class="data-value">{month_deals}</td>
671
+ </tr>
672
+ <tr style="background:#f5f5f5;">
673
+ <td>{deal_A_pct}%</td>
674
+ <td>{deal_B_pct}%</td>
675
+ <td>{deal_C_pct}%</td>
676
+ <td>{deal_D_pct}%</td>
677
+ <td>100%</td>
678
+ </tr>
679
+ </table>
680
+ </div>
681
+ </div>
682
+
683
+ <!-- 来访渠道分布 -->
684
+ <table>
685
+ <tr><td colspan="5" class="section-title" style="font-size:12px;">来访渠道分布(本月)</td></tr>
686
+ <tr class="table-header">
687
+ <td>渠道类型</td>
688
+ <td>来访组数</td>
689
+ <td>占比</td>
690
+ <td>意向等级分布</td>
691
+ <td>备注</td>
692
+ </tr>
693
+ {channel_rows}
694
+ <tr style="background:#1F4E79; color:#fff;">
695
+ <td><strong>合计</strong></td>
696
+ <td class="data-value" style="color:#fff;">{channel_total}</td>
697
+ <td>100%</td>
698
+ <td>A:{intent_A}, B:{intent_B}, C:{intent_C}, D:{intent_D}</td>
699
+ <td>-</td>
700
+ </tr>
701
+ </table>
702
+
703
+ <!-- 存量客户汇总 -->
704
+ <table>
705
+ <tr><td colspan="7" class="section-title" style="font-size:12px;">存量客户本周复访 - 近3月未成交({report_data['three_months_ago']} ~ {yesterday})</td></tr>
706
+ <tr class="table-header">
707
+ <td>渠道</td>
708
+ <td>未成交总数</td>
709
+ <td>A级</td>
710
+ <td>B级</td>
711
+ <td>C级</td>
712
+ <td>D级</td>
713
+ <td>占比</td>
714
+ </tr>
715
+ {stock_summary_rows}
716
+ </table>
717
+
718
+ <!-- 存量客户明细 -->
719
+ <table>
720
+ <tr><td colspan="12" class="section-title" style="font-size:12px; background:#C59BA6;">存量客户预计本周复访明细</td></tr>
721
+ <tr class="table-header-peach">
722
+ <td class="no">序号</td>
723
+ <td>客户姓名</td>
724
+ <td>来访渠道</td>
725
+ <td>置业顾问</td>
726
+ <td>最近来访</td>
727
+ <td>意向等级</td>
728
+ <td>跟进情况</td>
729
+ <td>决策周期</td>
730
+ <td>未成交原因</td>
731
+ <td>本周复访概率</td>
732
+ <td>复访成交概率</td>
733
+ <td>跟进策略</td>
734
+ </tr>
735
+ {stock_detail_rows}
736
+ <tr class="light-bg">
737
+ <td colspan="12" style="text-align:left; font-size:10px; color:#666;">
738
+ 注:以上为近3月有来访记录但未成交的客户,按最近来访时间排序。高意向(A级)客户{intent_A}组,建议重点跟进。
739
+ </td>
740
+ </tr>
741
+ </table>
742
+
743
+ <!-- 昨日来访明细 -->
744
+ <table>
745
+ <tr><td colspan="7" class="section-title" style="font-size:12px;">昨日来访明细({yesterday} 共{yesterday_visits}组)</td></tr>
746
+ <tr class="table-header">
747
+ <td class="no">序号</td>
748
+ <td>到访时间</td>
749
+ <td>顾问</td>
750
+ <td>到访次数</td>
751
+ <td>停留时长</td>
752
+ <td>首访/复访</td>
753
+ <td>备注</td>
754
+ </tr>
755
+ {yesterday_detail_rows}
756
+ <tr class="light-bg">
757
+ <td colspan="7" style="text-align:left; font-size:10px; color:#666;">
758
+ 来访分析:{yesterday_analysis}。
759
+ </td>
760
+ </tr>
761
+ </table>
762
+
763
+ <!-- 顾问来访统计 -->
764
+ <table>
765
+ <tr><td colspan="5" class="section-title" style="font-size:12px;">顾问来访统计(本月截至{yesterday})</td></tr>
766
+ <tr class="table-header">
767
+ <td>排名</td>
768
+ <td>顾问</td>
769
+ <td>昨日来访</td>
770
+ <td>本月来访</td>
771
+ <td>备注</td>
772
+ </tr>
773
+ {advisor_rows}
774
+ </table>
775
+
776
+ <!-- 数据说明 -->
777
+ <table>
778
+ <tr><td colspan="4" class="section-title" style="font-size:12px;">数据说明</td></tr>
779
+ <tr class="table-header-pink">
780
+ <td>数据项</td>
781
+ <td>数据来源</td>
782
+ <td>数据状态</td>
783
+ <td>备注</td>
784
+ </tr>
785
+ <tr>
786
+ <td>来访数据</td>
787
+ <td>旺小宝来访记录API</td>
788
+ <td class="data-value" style="color:#0a0;">已获取</td>
789
+ <td>{month_start}~{yesterday}共{month_visit_actual}组</td>
790
+ </tr>
791
+ <tr>
792
+ <td>意向等级</td>
793
+ <td>旺小宝客户画像API</td>
794
+ <td class="data-value" style="color:#0a0;">已获取</td>
795
+ <td>顾问评级(A/B/C/D)</td>
796
+ </tr>
797
+ <tr>
798
+ <td>渠道分类</td>
799
+ <td>旺小宝dynamicTags</td>
800
+ <td class="data-value" style="color:#0a0;">已获取</td>
801
+ <td>五大类来访渠道</td>
802
+ </tr>
803
+ <tr>
804
+ <td>成交数据</td>
805
+ <td>旺小宝客户画像API</td>
806
+ <td class="data-value" style="color:#f90;">待确认</td>
807
+ <td>本月成交{month_deals}组,金额{deal_amount}</td>
808
+ </tr>
809
+ <tr>
810
+ <td>月度KPI指标</td>
811
+ <td>IMA知识库/配置</td>
812
+ <td class="data-value" style="color:#0a0;">已配置</td>
813
+ <td>来访{month_visit_target}组/销售{month_sales_target}万</td>
814
+ </tr>
815
+ </table>
816
+
817
+ <!-- 页脚 -->
818
+ <div class="footer">
819
+ <p>{PROJECT_NAME} 营销日报 | 数据日期:{yesterday} | 生成时间:{today}</p>
820
+ <p>数据来源:旺小宝智能营销系统 | 本月来访{month_visit_actual}组 | 意向客户{intent_A}组(A级)</p>
821
+ <p style="color:#c00; font-weight:bold;">注:成交金额待从销售管理系统补充,建议每日同步</p>
822
+ </div>
823
+
824
+ </body>
825
+ </html>'''
826
+
827
+ return html
828
+
829
+
830
+ def generate_pdf(html_path, pdf_path):
831
+ """使用 Chrome/Edge headless 将 HTML 转为 PDF,排版与 HTML 完全一致"""
832
+ if _BROWSER_PATH is None:
833
+ print(" [PDF] 未找到 Chrome/Edge,跳过 PDF 生成")
834
+ print(" 请安装 Google Chrome 或 Microsoft Edge 浏览器")
835
+ return False
836
+ try:
837
+ # 用浏览器 headless 打印 PDF
838
+ cmd = [
839
+ _BROWSER_PATH,
840
+ "--headless",
841
+ "--disable-gpu",
842
+ "--no-sandbox",
843
+ "--print-background",
844
+ f"--print-to-pdf={pdf_path}",
845
+ f"file:///{html_path.replace(chr(92), '/')}"
846
+ ]
847
+ # 过滤掉可能导致编码问题的环境
848
+ env = os.environ.copy()
849
+ env.pop("PYTHONIOENCODING", None)
850
+ result = subprocess.run(
851
+ cmd,
852
+ capture_output=True,
853
+ text=True,
854
+ encoding="utf-8",
855
+ errors="replace",
856
+ env=env,
857
+ timeout=60
858
+ )
859
+ if result.returncode == 0 and os.path.exists(pdf_path):
860
+ print(f" [PDF] 已生成: {pdf_path}")
861
+ return True
862
+ else:
863
+ print(f" [PDF] 浏览器打印失败: {result.stderr[:200]}")
864
+ return False
865
+ except Exception as e:
866
+ print(f" [PDF] 生成失败: {e}")
867
+ return False
868
+
869
+
870
+ def main(date_str=None):
871
+ """主函数"""
872
+ if date_str is None:
873
+ today = datetime.now().strftime('%Y-%m-%d')
874
+ yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
875
+ else:
876
+ today = datetime.now().strftime('%Y-%m-%d')
877
+ yesterday = date_str
878
+
879
+ # 月份第一天
880
+ month_start = yesterday[:7] + '-01'
881
+ # 3个月前
882
+ three_months_ago = (datetime.strptime(yesterday, '%Y-%m-%d') - timedelta(days=90)).strftime('%Y-%m-%d')
883
+
884
+ print("=" * 60)
885
+ print(f"{PROJECT_NAME} 日报生成 - {yesterday}")
886
+ print("=" * 60)
887
+
888
+ # 检查token
889
+ if not os.path.exists(TOKEN_PATH):
890
+ print("ERROR: 未找到旺小宝授权token,请先授权")
891
+ print("提示:系统会引导你完成OAuth授权")
892
+ return None
893
+
894
+ token = get_token()
895
+
896
+ try:
897
+ # 1. 昨日来访
898
+ print("\n[1] 拉取昨日来访数据...")
899
+ yesterday_visits = get_yesterday_visits(token, yesterday)
900
+ print(f" 昨日来访: {len(yesterday_visits)}组")
901
+
902
+ # 2. 本月来访
903
+ print("\n[2] 拉取本月来访数据...")
904
+ month_visits = get_month_visits(token, month_start, yesterday)
905
+ print(f" 本月来访: {len(month_visits)}组")
906
+
907
+ # 3. 客户画像
908
+ print("\n[3] 拉取客户画像数据...")
909
+ customers = get_customers(token, month_start, yesterday)
910
+ print(f" 本月客户总数: {len(customers)}")
911
+
912
+ # 意向等级
913
+ intent_dist = analyze_intent(customers)
914
+ print("\n 意向等级分布:")
915
+ for level, cnt in sorted(intent_dist.items(), key=lambda x: -x[1]):
916
+ if cnt > 0:
917
+ print(f" {level}: {cnt}组")
918
+
919
+ # 渠道分布
920
+ channel_dist = analyze_channel(customers)
921
+ print("\n 渠道分布:")
922
+ for ch, cnt in sorted(channel_dist.items(), key=lambda x: -x[1]):
923
+ print(f" {ch}: {cnt}组")
924
+
925
+ # 4. 存量客户
926
+ print("\n[4] 拉取存量客户数据...")
927
+ stock_customers = get_stock_customers(token, three_months_ago, yesterday)
928
+ print(f" 存量客户(未成交): {len(stock_customers)}组")
929
+
930
+ # 5. 成交客户
931
+ print("\n[5] 分析成交数据...")
932
+ deal_customers = [c for c in customers if c.get('dealStatus') == 1]
933
+ deal_intent = analyze_intent(deal_customers)
934
+ print(f" 本月成交: {len(deal_customers)}组")
935
+
936
+ # 6. 存量客户按渠道分析
937
+ stock_by_channel = analyze_channel_with_intent(stock_customers)
938
+
939
+ # 整理来访明细
940
+ yesterday_visit_list = [{
941
+ 'time': v['visitTime'].split(' ')[1] if ' ' in v.get('visitTime', '') else v.get('visitTime', ''),
942
+ 'advisor': v.get('userName', ''),
943
+ 'count': v.get('visitCount', 1),
944
+ 'duration': int(int(v.get('visitTimer', '0')) / 60000) if v.get('visitTimer', '0') != '0' else 0
945
+ } for v in yesterday_visits]
946
+
947
+ # 构建报告数据
948
+ report_data = {
949
+ 'date': today,
950
+ 'yesterday': yesterday,
951
+ 'month_start': month_start,
952
+ 'three_months_ago': three_months_ago,
953
+ 'project_name': PROJECT_NAME,
954
+ 'month_visit_target': KPI_CONFIG['month_visit_target'],
955
+ 'month_visit_actual': len(month_visits),
956
+ 'yesterday_visits': len(yesterday_visits),
957
+ 'yesterday_visit_list': yesterday_visit_list,
958
+ 'month_sales_target': KPI_CONFIG['month_sales_target'],
959
+ 'month_deals': len(deal_customers),
960
+ 'deal_amount': '待填报',
961
+ 'intent_dist': intent_dist,
962
+ 'channel_dist': channel_dist,
963
+ 'stock_customers': stock_customers[:10], # 只取前10条
964
+ 'stock_by_channel': stock_by_channel,
965
+ 'deal_intent': deal_intent
966
+ }
967
+
968
+ # 生成HTML
969
+ print("\n[6] 生成HTML日报...")
970
+ html_content = generate_html(report_data)
971
+
972
+ # 保存 HTML 文件
973
+ date_str_short = yesterday.replace('-', '')
974
+ html_path = os.path.join(OUTPUT_DIR, f"{PROJECT_NAME}_日报_{date_str_short}.html")
975
+ with open(html_path, 'w', encoding='utf-8') as f:
976
+ f.write(html_content)
977
+ print(f"\nHTML日报已生成: {html_path}")
978
+
979
+ # 同时生成 PDF
980
+ pdf_path = os.path.splitext(html_path)[0] + '.pdf'
981
+ print("\n[7] 生成PDF日报...")
982
+ pdf_ok = generate_pdf(html_path, pdf_path)
983
+ if pdf_ok:
984
+ print(f"PDF日报已生成: {pdf_path}")
985
+ else:
986
+ print(" PDF生成跳过(浏览器不可用)")
987
+ print("\n" + "=" * 60)
988
+ print("数据摘要")
989
+ print("=" * 60)
990
+ print(f"本月来访: {len(month_visits)}组 (目标{KPI_CONFIG['month_visit_target']}组)")
991
+ print(f"昨日来访: {len(yesterday_visits)}组")
992
+ print(f"本月成交: {len(deal_customers)}组")
993
+ print(f"存量客户: {len(stock_customers)}组")
994
+ print(f"\nHTML: {html_path}")
995
+ if pdf_ok:
996
+ print(f"PDF: {pdf_path}")
997
+
998
+ result = (html_path, pdf_path) if pdf_ok else (html_path, None)
999
+ return result
1000
+
1001
+ except Exception as e:
1002
+ print(f"\nERROR: {str(e)}")
1003
+ import traceback
1004
+ traceback.print_exc()
1005
+ return None
1006
+
1007
+
1008
+ if __name__ == "__main__":
1009
+ parser = argparse.ArgumentParser(description="旺小宝项目日报生成")
1010
+ parser.add_argument("date", nargs="?", default=None, help="日期 YYYY-MM-DD(默认昨日)")
1011
+ parser.add_argument("--tenant-id", help="旺小宝租户ID(覆盖环境变量)")
1012
+ parser.add_argument("--project-id", help="旺小宝项目ID(覆盖环境变量)")
1013
+ parser.add_argument("--project-name", help="项目名称(覆盖环境变量)")
1014
+ parser.add_argument("--month-visit-target", type=int, help="月度来访目标")
1015
+ parser.add_argument("--month-sales-target", type=int, help="月度销售目标(万元)")
1016
+ args = parser.parse_args()
1017
+
1018
+ if args.tenant_id:
1019
+ TENANT_ID = args.tenant_id
1020
+ if args.project_id:
1021
+ PROJECT_ID = args.project_id
1022
+ if args.project_name:
1023
+ PROJECT_NAME = args.project_name
1024
+ if args.month_visit_target is not None:
1025
+ KPI_CONFIG["month_visit_target"] = args.month_visit_target
1026
+ if args.month_sales_target is not None:
1027
+ KPI_CONFIG["month_sales_target"] = args.month_sales_target
1028
+
1029
+ main(args.date)