sophhub 0.4.21 → 0.4.22
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/package.json +1 -1
- package/skills/insurance-customer-policy/skill.json +12 -0
- package/skills/insurance-customer-policy/src/SKILL.md +121 -0
- package/skills/insurance-customer-policy/src/pyproject.toml +9 -0
- package/skills/insurance-customer-policy/src/scripts/cli.py +16 -0
- package/skills/insurance-customer-policy/src/scripts/cloud_insurance_full_test.py +785 -0
- package/skills/insurance-customer-policy/src/scripts/dashboard_all.py +205 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__init__.py +0 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__main__.py +4 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/cli.py +816 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/db.py +181 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/insurance_paths.py +184 -0
- package/skills/insurance-customer-policy/src/scripts/run_e2e_smoke.py +164 -0
- package/skills/insurance-customer-policy/src/scripts/test_cloud_zero_config.py +217 -0
- package/skills/insurance-customer-policy/src/scripts/test_dashboard_all.py +113 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__init__.py +0 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__main__.py +4 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/cli.py +816 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/db.py +181 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/insurance_paths.py +184 -0
- package/skills/insurance-product-analysis/skill.json +12 -0
- package/skills/insurance-product-analysis/src/SKILL.md +99 -0
- package/skills/insurance-product-analysis/src/pyproject.toml +9 -0
- package/skills/insurance-product-analysis/src/scripts/cli.py +16 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__init__.py +0 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__main__.py +4 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/cli.py +545 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/db.py +180 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/orchestrator.py +163 -0
- package/skills/insurance-product-analysis/src/scripts/run_e2e_smoke.py +202 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/__init__.py +0 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/__main__.py +4 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/cli.py +545 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/db.py +180 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/orchestrator.py +163 -0
- package/skills/insurance-sales-pipeline/skill.json +12 -0
- package/skills/insurance-sales-pipeline/src/SKILL.md +102 -0
- package/skills/insurance-sales-pipeline/src/pyproject.toml +9 -0
- package/skills/insurance-sales-pipeline/src/scripts/cli.py +16 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__init__.py +0 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__main__.py +4 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/cli.py +496 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/db.py +180 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/orchestrator.py +36 -0
- package/skills/insurance-sales-pipeline/src/scripts/run_e2e_smoke.py +208 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__init__.py +0 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__main__.py +4 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/cli.py +496 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/db.py +180 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/orchestrator.py +36 -0
- package/skills/insurance-schedule-renewal/skill.json +12 -0
- package/skills/insurance-schedule-renewal/src/SKILL.md +94 -0
- package/skills/insurance-schedule-renewal/src/pyproject.toml +9 -0
- package/skills/insurance-schedule-renewal/src/scripts/cli.py +16 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__init__.py +0 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__main__.py +4 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/cli.py +429 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/db.py +180 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/orchestrator.py +94 -0
- package/skills/insurance-schedule-renewal/src/scripts/run_e2e_smoke.py +218 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__init__.py +0 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__main__.py +4 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/cli.py +429 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/db.py +180 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/orchestrator.py +94 -0
- package/skills/insurance-shared-data/skill.json +20 -0
- package/skills/insurance-shared-data/src/SKILL.md +33 -0
- package/skills/insurance-shared-data/src/scripts/cloud_insurance_super_test.py +246 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Shared-db read helpers for insurance-sales-pipeline."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from insurance_pipeline_cli import db
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def customer_exists(customer_id: str) -> bool:
|
|
8
|
+
conn = db.connect()
|
|
9
|
+
try:
|
|
10
|
+
row = conn.execute(
|
|
11
|
+
"SELECT 1 FROM customers WHERE id=? AND status='active'",
|
|
12
|
+
(customer_id,),
|
|
13
|
+
).fetchone()
|
|
14
|
+
return bool(row)
|
|
15
|
+
finally:
|
|
16
|
+
conn.close()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def customer_query(customer_id: str) -> dict:
|
|
20
|
+
conn = db.connect()
|
|
21
|
+
try:
|
|
22
|
+
rows = list(
|
|
23
|
+
conn.execute("SELECT * FROM customers WHERE id=? AND status='active'", (customer_id,))
|
|
24
|
+
)
|
|
25
|
+
return {"ok": True, "customers": [dict(r) for r in rows], "count": len(rows)}
|
|
26
|
+
finally:
|
|
27
|
+
conn.close()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def product_query(name: str) -> dict:
|
|
31
|
+
conn = db.connect()
|
|
32
|
+
try:
|
|
33
|
+
rows = list(conn.execute("SELECT * FROM products WHERE name=? AND status='active'", (name,)))
|
|
34
|
+
return {"ok": True, "products": [dict(r) for r in rows], "count": len(rows)}
|
|
35
|
+
finally:
|
|
36
|
+
conn.close()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "insurance-schedule-renewal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"types": ["store"],
|
|
5
|
+
"displayName": "保险日程与续保提醒",
|
|
6
|
+
"description": "续保到期、跟进、沉睡客户唤醒、生日/周年日提醒。瘦 skill:所有业务数据通过 subprocess 调 insurance-customer-policy 实时取,仅本地维护提醒去重表。",
|
|
7
|
+
"changelog": [
|
|
8
|
+
{ "version": "1.0.0", "date": "2026-05-09", "changes": ["初版 CLI:renewal/followup/dormant/special-date/daily-summary"] }
|
|
9
|
+
],
|
|
10
|
+
"createdAt": "2026-05-09",
|
|
11
|
+
"updatedAt": "2026-05-09"
|
|
12
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: insurance-schedule-renewal
|
|
3
|
+
description: "保险经纪人日程与续保提醒:续保到期、跟进提醒、沉睡客户唤醒、客户生日 / 保单周年日。所有业务数据直接读写统一保险共享库(insurance.sqlite3)。触发:续保提醒、哪些保单到期、哪些客户该联系、沉睡客户、生日提醒、周年日、看下数据。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 日程与续保提醒
|
|
7
|
+
|
|
8
|
+
## 经营概览("看下数据")
|
|
9
|
+
|
|
10
|
+
聚合脚本由 `insurance-customer-policy` 提供,直接读取共享库:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python3 {baseDir}/../../insurance-customer-policy/src/scripts/dashboard_all.py --md
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
本 skill 的 `daily-summary` 已内嵌相同维度的提醒明细,可单独使用。
|
|
17
|
+
|
|
18
|
+
## 运行方式
|
|
19
|
+
|
|
20
|
+
**推荐(云端零配置 / 跨平台)**:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python3 {baseDir}/scripts/cli.py <子命令> ...
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**高级用法**:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Linux / macOS
|
|
30
|
+
export PYTHONPATH="{baseDir}/src"
|
|
31
|
+
python3 -m insurance_schedule_cli <子命令> ...
|
|
32
|
+
|
|
33
|
+
# Windows
|
|
34
|
+
cd "{baseDir}"
|
|
35
|
+
set PYTHONPATH=%CD%\src
|
|
36
|
+
python -m insurance_schedule_cli <子命令> ...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
默认数据库:
|
|
40
|
+
|
|
41
|
+
- Linux / macOS:`~/.config/insurance/insurance.sqlite3`
|
|
42
|
+
- Windows:`%USERPROFILE%\.config\insurance\insurance.sqlite3`
|
|
43
|
+
- 统一变量:`INSURANCE_DB_PATH`
|
|
44
|
+
- 兼容变量:`INSURANCE_SCHEDULE_DB_PATH`
|
|
45
|
+
|
|
46
|
+
共享库包含客户、保单、产品、销售、提醒去重等全量表;本 skill 直接读取,不依赖其他 skill 进程。
|
|
47
|
+
|
|
48
|
+
## 子命令
|
|
49
|
+
|
|
50
|
+
| 子命令 | 说明 |
|
|
51
|
+
|--------|------|
|
|
52
|
+
| `renewal-query --days N` | 取 N 天内到期或下次缴费的保单 |
|
|
53
|
+
| `renewal-remind --days N [--mark-sent]` | 列出未发过的续保提醒;`--mark-sent` 标记已发,同一保单同一桶不会重复 |
|
|
54
|
+
| `followup-check --days N` | 超过 N 天未跟进的客户 |
|
|
55
|
+
| `followup-remind --days N [--mark-sent]` | 列出未发过的跟进提醒并去重 |
|
|
56
|
+
| `dormant-query [--days 60]` | 沉睡客户列表 |
|
|
57
|
+
| `dormant-wake [--days 60]` | 沉睡客户 + 唤醒话术建议(按 续保 / 缺口 / 服务关怀 三种角度) |
|
|
58
|
+
| `special-date-query --type birthday\|anniversary [--month N]` | 客户生日 / 保单周年日 |
|
|
59
|
+
| `daily-summary` | 输出 5 类汇总数 + Markdown 卡片,对应需求文档 `xuqiu/baoxian/richeng.md`(L102-111) |
|
|
60
|
+
| `dashboard --json` | 数字版概览(供入口聚合) |
|
|
61
|
+
|
|
62
|
+
## 示例
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python3 {baseDir}/scripts/cli.py renewal-query --days 30
|
|
66
|
+
python3 {baseDir}/scripts/cli.py renewal-remind --days 30 --mark-sent
|
|
67
|
+
python3 {baseDir}/scripts/cli.py followup-check --days 30
|
|
68
|
+
python3 {baseDir}/scripts/cli.py dormant-wake --days 60
|
|
69
|
+
python3 {baseDir}/scripts/cli.py special-date-query --type birthday --month 5
|
|
70
|
+
python3 {baseDir}/scripts/cli.py daily-summary
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 自然语言路由
|
|
74
|
+
|
|
75
|
+
| 用户说 | 调用命令 |
|
|
76
|
+
|--------|----------|
|
|
77
|
+
| "哪些保单30天内到期?" | `renewal-query --days 30` |
|
|
78
|
+
| "哪些客户该联系了?" | `followup-remind --days 30` |
|
|
79
|
+
| "哪些客户超过一个月没联系了?" | `followup-check --days 30` |
|
|
80
|
+
| "沉睡客户有哪些?" | `dormant-query --days 60` |
|
|
81
|
+
| "唤醒沉睡客户" | `dormant-wake --days 60` |
|
|
82
|
+
| "这个月谁过生日?" | `special-date-query --type birthday` |
|
|
83
|
+
| "这个月有哪些保单周年日?" | `special-date-query --type anniversary` |
|
|
84
|
+
| "今日提醒" / "看下数据" | `daily-summary` |
|
|
85
|
+
|
|
86
|
+
## 去重机制
|
|
87
|
+
|
|
88
|
+
`renewal-remind` / `followup-remind` 写 `sent_reminders(reminder_type, target_id, bucket)` 唯一键。同一条提醒在同一桶(如 30d)内已发过会出现在响应的 `already_sent` 中。
|
|
89
|
+
|
|
90
|
+
## 与其他 Skill 协作
|
|
91
|
+
|
|
92
|
+
- 与客户、产品、销售 skill 共用同一 `insurance.sqlite3`,天然数据实时一致。
|
|
93
|
+
- 唤醒话术风格参照 `insurance-policy-review` 的合规边界与「续保提醒(温和版/强节点版)」模板。
|
|
94
|
+
- 可选增强:通过 `schedule-reminder` 注册 cron,每日早上自动跑 `daily-summary` 并通过 `--system-event --session main` 推送回当前对话。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cloud-friendly entrypoint for insurance-schedule-renewal."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
9
|
+
if str(SCRIPTS_DIR) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
11
|
+
|
|
12
|
+
from insurance_schedule_cli.cli import main
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
File without changes
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""Insurance schedule & renewal reminder CLI (thin skill).
|
|
2
|
+
|
|
3
|
+
All business data is read from shared insurance.sqlite3.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import sqlite3
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from insurance_schedule_cli import db, orchestrator
|
|
15
|
+
|
|
16
|
+
# Ensure UTF-8 stdout/stderr regardless of host shell codepage (Windows GBK etc.)
|
|
17
|
+
try:
|
|
18
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
19
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
20
|
+
except (AttributeError, OSError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def print_json(obj: Any) -> None:
|
|
25
|
+
print(json.dumps(obj, ensure_ascii=False, indent=2))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def today() -> datetime:
|
|
29
|
+
return datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fmt_date(d: datetime) -> str:
|
|
33
|
+
return d.strftime("%Y-%m-%d")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_dt(s: str | None) -> datetime | None:
|
|
37
|
+
if not s:
|
|
38
|
+
return None
|
|
39
|
+
try:
|
|
40
|
+
return datetime.strptime(s.strip()[:10], "%Y-%m-%d")
|
|
41
|
+
except ValueError:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------- renewal ----------
|
|
46
|
+
|
|
47
|
+
def _renewal_items(days: int) -> list[dict]:
|
|
48
|
+
"""One reminder per policy; pick the earliest of (expire_date, next_pay_date) inside horizon."""
|
|
49
|
+
horizon = today() + timedelta(days=int(days))
|
|
50
|
+
horizon_str = fmt_date(horizon)
|
|
51
|
+
by_expire = orchestrator.policy_query(expire_before=horizon_str)
|
|
52
|
+
by_pay = orchestrator.policy_query(next_pay_before=horizon_str)
|
|
53
|
+
best: dict[str, dict] = {}
|
|
54
|
+
for src, key in [(by_expire, "expire"), (by_pay, "next_pay")]:
|
|
55
|
+
for p in src.get("policies") or []:
|
|
56
|
+
pid = p.get("id")
|
|
57
|
+
if not pid:
|
|
58
|
+
continue
|
|
59
|
+
target_date = p.get("expire_date") if key == "expire" else p.get("next_pay_date")
|
|
60
|
+
tdt = parse_dt(target_date)
|
|
61
|
+
if not tdt or tdt < today() or tdt > horizon:
|
|
62
|
+
continue
|
|
63
|
+
days_remaining = (tdt - today()).days
|
|
64
|
+
existing = best.get(pid)
|
|
65
|
+
if existing and existing["days_remaining"] <= days_remaining:
|
|
66
|
+
continue
|
|
67
|
+
cust_d = orchestrator.customer_query(customer_id=p.get("customer_id"))
|
|
68
|
+
cust = (cust_d.get("customers") or [{}])[0]
|
|
69
|
+
best[pid] = {
|
|
70
|
+
"policy_id": pid,
|
|
71
|
+
"customer_id": p.get("customer_id"),
|
|
72
|
+
"customer_name": cust.get("name") or "",
|
|
73
|
+
"company": p.get("company"),
|
|
74
|
+
"product_name": p.get("product_name"),
|
|
75
|
+
"premium": p.get("premium"),
|
|
76
|
+
"kind": key,
|
|
77
|
+
"target_date": target_date,
|
|
78
|
+
"days_remaining": days_remaining,
|
|
79
|
+
}
|
|
80
|
+
items = list(best.values())
|
|
81
|
+
items.sort(key=lambda x: x["days_remaining"])
|
|
82
|
+
return items
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _bucket_for_days(days: int) -> str:
|
|
86
|
+
return f"{int(days)}d"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _check_sent(conn: sqlite3.Connection, rtype: str, target: str, bucket: str) -> bool:
|
|
90
|
+
row = conn.execute(
|
|
91
|
+
"SELECT 1 FROM sent_reminders WHERE reminder_type=? AND target_id=? AND bucket=?",
|
|
92
|
+
(rtype, target, bucket),
|
|
93
|
+
).fetchone()
|
|
94
|
+
return bool(row)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _mark_sent(conn: sqlite3.Connection, rtype: str, target: str, bucket: str, note: str = "") -> bool:
|
|
98
|
+
try:
|
|
99
|
+
conn.execute(
|
|
100
|
+
"INSERT INTO sent_reminders (reminder_type, target_id, bucket, note) VALUES (?,?,?,?)",
|
|
101
|
+
(rtype, target, bucket, note),
|
|
102
|
+
)
|
|
103
|
+
conn.commit()
|
|
104
|
+
return True
|
|
105
|
+
except sqlite3.IntegrityError:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cmd_renewal_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
110
|
+
items = _renewal_items(int(args.days))
|
|
111
|
+
print_json({"ok": True, "days": int(args.days), "items": items, "count": len(items)})
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def cmd_renewal_remind(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
116
|
+
items = _renewal_items(int(args.days))
|
|
117
|
+
bucket = _bucket_for_days(args.days)
|
|
118
|
+
new_items = []
|
|
119
|
+
skipped = []
|
|
120
|
+
for it in items:
|
|
121
|
+
if _check_sent(conn, "renewal", it["policy_id"], bucket):
|
|
122
|
+
skipped.append(it)
|
|
123
|
+
continue
|
|
124
|
+
if args.mark_sent:
|
|
125
|
+
_mark_sent(conn, "renewal", it["policy_id"], bucket, note=it.get("kind", ""))
|
|
126
|
+
new_items.append(it)
|
|
127
|
+
print_json({"ok": True, "bucket": bucket, "new": new_items, "already_sent": skipped,
|
|
128
|
+
"new_count": len(new_items), "skipped_count": len(skipped)})
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------- followup ----------
|
|
133
|
+
|
|
134
|
+
def _followup_lapsed(days_threshold: int) -> list[dict]:
|
|
135
|
+
cust = orchestrator.customer_query()
|
|
136
|
+
fu = orchestrator.followup_query()
|
|
137
|
+
last_by_cust: dict[str, str] = {}
|
|
138
|
+
for f in fu.get("followups") or []:
|
|
139
|
+
cid = f.get("customer_id")
|
|
140
|
+
ts = f.get("created_at")
|
|
141
|
+
if cid and ts and (cid not in last_by_cust or ts > last_by_cust[cid]):
|
|
142
|
+
last_by_cust[cid] = ts
|
|
143
|
+
out: list[dict] = []
|
|
144
|
+
for c in cust.get("customers") or []:
|
|
145
|
+
cid = c["id"]
|
|
146
|
+
last = last_by_cust.get(cid) or c.get("last_followup_at")
|
|
147
|
+
if not last:
|
|
148
|
+
try:
|
|
149
|
+
created = datetime.strptime(c["created_at"][:19], "%Y-%m-%d %H:%M:%S")
|
|
150
|
+
except (ValueError, TypeError):
|
|
151
|
+
continue
|
|
152
|
+
days_since = (datetime.now() - created).days
|
|
153
|
+
last_str = c.get("created_at")
|
|
154
|
+
else:
|
|
155
|
+
try:
|
|
156
|
+
last_dt = datetime.strptime(last[:19], "%Y-%m-%d %H:%M:%S")
|
|
157
|
+
except ValueError:
|
|
158
|
+
continue
|
|
159
|
+
days_since = (datetime.now() - last_dt).days
|
|
160
|
+
last_str = last
|
|
161
|
+
if days_since >= int(days_threshold):
|
|
162
|
+
out.append({
|
|
163
|
+
"customer_id": cid,
|
|
164
|
+
"customer_name": c.get("name"),
|
|
165
|
+
"phone": c.get("phone"),
|
|
166
|
+
"days_since_followup": days_since,
|
|
167
|
+
"last_followup_at": last_str,
|
|
168
|
+
})
|
|
169
|
+
out.sort(key=lambda x: x["days_since_followup"], reverse=True)
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cmd_followup_check(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
174
|
+
items = _followup_lapsed(int(args.days))
|
|
175
|
+
print_json({"ok": True, "days": int(args.days), "items": items, "count": len(items)})
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cmd_followup_remind(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
180
|
+
items = _followup_lapsed(int(args.days))
|
|
181
|
+
bucket = _bucket_for_days(args.days)
|
|
182
|
+
new_items, skipped = [], []
|
|
183
|
+
for it in items:
|
|
184
|
+
if _check_sent(conn, "followup", it["customer_id"], bucket):
|
|
185
|
+
skipped.append(it)
|
|
186
|
+
continue
|
|
187
|
+
if args.mark_sent:
|
|
188
|
+
_mark_sent(conn, "followup", it["customer_id"], bucket)
|
|
189
|
+
new_items.append(it)
|
|
190
|
+
print_json({"ok": True, "bucket": bucket, "new": new_items, "already_sent": skipped,
|
|
191
|
+
"new_count": len(new_items), "skipped_count": len(skipped)})
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------- dormant ----------
|
|
196
|
+
|
|
197
|
+
def cmd_dormant_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
198
|
+
items = _followup_lapsed(int(args.days))
|
|
199
|
+
print_json({"ok": True, "days": int(args.days), "items": items, "count": len(items)})
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
WAKE_TEMPLATES = {
|
|
204
|
+
"renewal": "{name} 您好,您在 {company} 的{product}快到期了,正好趁此机会一起复盘下保障情况,有什么调整想做的吗?",
|
|
205
|
+
"gap": "{name} 您好,最近梳理客户档案时注意到您还差{gap}方面的保障,有空我们聊 5 分钟看看怎么补齐?",
|
|
206
|
+
"service": "{name} 您好,最近天气变化大,提醒您注意身体;顺便跟您同步一下今年理赔体验上的几个小变化。",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cmd_dormant_wake(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
211
|
+
items = _followup_lapsed(int(args.days))
|
|
212
|
+
suggestions: list[dict] = []
|
|
213
|
+
for it in items:
|
|
214
|
+
cid = it["customer_id"]
|
|
215
|
+
gap = orchestrator.gap_analysis(customer_id=cid)
|
|
216
|
+
gaps = (gap.get("result") or {}).get("gaps") or []
|
|
217
|
+
renewals = _renewal_items(60)
|
|
218
|
+
my_renewals = [r for r in renewals if r["customer_id"] == cid]
|
|
219
|
+
if my_renewals:
|
|
220
|
+
r0 = my_renewals[0]
|
|
221
|
+
tpl = WAKE_TEMPLATES["renewal"].format(name=it["customer_name"],
|
|
222
|
+
company=r0["company"], product=r0["product_name"])
|
|
223
|
+
angle = "renewal"
|
|
224
|
+
elif gaps:
|
|
225
|
+
tpl = WAKE_TEMPLATES["gap"].format(name=it["customer_name"], gap="、".join(gaps[:2]))
|
|
226
|
+
angle = "gap"
|
|
227
|
+
else:
|
|
228
|
+
tpl = WAKE_TEMPLATES["service"].format(name=it["customer_name"])
|
|
229
|
+
angle = "service"
|
|
230
|
+
suggestions.append({
|
|
231
|
+
"customer_id": cid,
|
|
232
|
+
"customer_name": it["customer_name"],
|
|
233
|
+
"phone": it.get("phone"),
|
|
234
|
+
"days_since_followup": it["days_since_followup"],
|
|
235
|
+
"gaps": gaps,
|
|
236
|
+
"angle": angle,
|
|
237
|
+
"script": tpl,
|
|
238
|
+
})
|
|
239
|
+
print_json({"ok": True, "days": int(args.days), "items": suggestions, "count": len(suggestions)})
|
|
240
|
+
return 0
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------- special-date ----------
|
|
244
|
+
|
|
245
|
+
def cmd_special_date_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
246
|
+
month = int(args.month) if args.month else datetime.now().month
|
|
247
|
+
out: list[dict] = []
|
|
248
|
+
if args.type == "birthday":
|
|
249
|
+
cust = orchestrator.customer_query()
|
|
250
|
+
for c in cust.get("customers") or []:
|
|
251
|
+
bday = c.get("birthday")
|
|
252
|
+
if not bday:
|
|
253
|
+
continue
|
|
254
|
+
try:
|
|
255
|
+
m = int(str(bday)[5:7])
|
|
256
|
+
except (ValueError, TypeError):
|
|
257
|
+
continue
|
|
258
|
+
if m == month:
|
|
259
|
+
out.append({
|
|
260
|
+
"customer_id": c["id"],
|
|
261
|
+
"customer_name": c["name"],
|
|
262
|
+
"phone": c.get("phone"),
|
|
263
|
+
"birthday": bday,
|
|
264
|
+
})
|
|
265
|
+
elif args.type == "anniversary":
|
|
266
|
+
pol = orchestrator.policy_query()
|
|
267
|
+
for p in pol.get("policies") or []:
|
|
268
|
+
eff = p.get("effective_date")
|
|
269
|
+
if not eff:
|
|
270
|
+
continue
|
|
271
|
+
try:
|
|
272
|
+
m = int(str(eff)[5:7])
|
|
273
|
+
except (ValueError, TypeError):
|
|
274
|
+
continue
|
|
275
|
+
if m == month:
|
|
276
|
+
cust_d = orchestrator.customer_query(customer_id=p.get("customer_id"))
|
|
277
|
+
cust = (cust_d.get("customers") or [{}])[0]
|
|
278
|
+
eff_dt = parse_dt(eff)
|
|
279
|
+
years = (datetime.now().year - eff_dt.year) if eff_dt else None
|
|
280
|
+
out.append({
|
|
281
|
+
"policy_id": p["id"],
|
|
282
|
+
"customer_id": p.get("customer_id"),
|
|
283
|
+
"customer_name": cust.get("name"),
|
|
284
|
+
"company": p.get("company"),
|
|
285
|
+
"product_name": p.get("product_name"),
|
|
286
|
+
"effective_date": eff,
|
|
287
|
+
"years": years,
|
|
288
|
+
})
|
|
289
|
+
else:
|
|
290
|
+
print_json({"ok": False, "error": "bad_type", "hint": "type 必须是 birthday 或 anniversary"})
|
|
291
|
+
return 1
|
|
292
|
+
print_json({"ok": True, "type": args.type, "month": month, "items": out, "count": len(out)})
|
|
293
|
+
return 0
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ---------- daily-summary / dashboard ----------
|
|
297
|
+
|
|
298
|
+
def _summarize() -> dict:
|
|
299
|
+
renewals_30 = _renewal_items(30)
|
|
300
|
+
followup_30 = _followup_lapsed(30)
|
|
301
|
+
dormant_60 = _followup_lapsed(60)
|
|
302
|
+
bday = orchestrator.customer_query()
|
|
303
|
+
month = datetime.now().month
|
|
304
|
+
bday_count = 0
|
|
305
|
+
for c in bday.get("customers") or []:
|
|
306
|
+
try:
|
|
307
|
+
if c.get("birthday") and int(str(c["birthday"])[5:7]) == month:
|
|
308
|
+
bday_count += 1
|
|
309
|
+
except (ValueError, TypeError):
|
|
310
|
+
pass
|
|
311
|
+
pol = orchestrator.policy_query()
|
|
312
|
+
anniv_count = 0
|
|
313
|
+
for p in pol.get("policies") or []:
|
|
314
|
+
try:
|
|
315
|
+
if p.get("effective_date") and int(str(p["effective_date"])[5:7]) == month:
|
|
316
|
+
anniv_count += 1
|
|
317
|
+
except (ValueError, TypeError):
|
|
318
|
+
pass
|
|
319
|
+
return {
|
|
320
|
+
"renewals_30d": renewals_30,
|
|
321
|
+
"followup_30d": followup_30,
|
|
322
|
+
"dormant_60d": dormant_60,
|
|
323
|
+
"birthday_this_month": bday_count,
|
|
324
|
+
"anniversary_this_month": anniv_count,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def cmd_daily_summary(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
329
|
+
s = _summarize()
|
|
330
|
+
today_str = datetime.now().strftime("%Y-%m-%d")
|
|
331
|
+
md_lines = [f"⏰ **今日提醒({today_str})**", ""]
|
|
332
|
+
md_lines.append(f"📋 续保提醒:{len(s['renewals_30d'])} 份保单 30 天内到期/缴费")
|
|
333
|
+
for it in s["renewals_30d"][:5]:
|
|
334
|
+
md_lines.append(f" - {it['customer_name']} - {it['product_name']} - {it['target_date']}")
|
|
335
|
+
md_lines.append(f"📞 跟进提醒:{len(s['followup_30d'])} 位客户超 30 天未联系")
|
|
336
|
+
md_lines.append(f"💤 沉睡唤醒:{len(s['dormant_60d'])} 位客户超 60 天未联系")
|
|
337
|
+
md_lines.append(f"🎂 客户生日:{s['birthday_this_month']} 位")
|
|
338
|
+
md_lines.append(f"🎊 保单周年日:{s['anniversary_this_month']} 位")
|
|
339
|
+
print_json({
|
|
340
|
+
"ok": True,
|
|
341
|
+
"summary": {
|
|
342
|
+
"renewal_30d": len(s["renewals_30d"]),
|
|
343
|
+
"followup_30d_lapsed": len(s["followup_30d"]),
|
|
344
|
+
"dormant_60d": len(s["dormant_60d"]),
|
|
345
|
+
"birthday_this_month": s["birthday_this_month"],
|
|
346
|
+
"anniversary_this_month": s["anniversary_this_month"],
|
|
347
|
+
},
|
|
348
|
+
"details": s,
|
|
349
|
+
"markdown": "\n".join(md_lines),
|
|
350
|
+
})
|
|
351
|
+
return 0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def cmd_dashboard(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
355
|
+
s = _summarize()
|
|
356
|
+
print_json({
|
|
357
|
+
"ok": True,
|
|
358
|
+
"skill": "insurance-schedule-renewal",
|
|
359
|
+
"renewal_30d": len(s["renewals_30d"]),
|
|
360
|
+
"followup_30d_lapsed": len(s["followup_30d"]),
|
|
361
|
+
"dormant_60d": len(s["dormant_60d"]),
|
|
362
|
+
"birthday_this_month": s["birthday_this_month"],
|
|
363
|
+
"anniversary_this_month": s["anniversary_this_month"],
|
|
364
|
+
})
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ---------- argparse ----------
|
|
369
|
+
|
|
370
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
371
|
+
p = argparse.ArgumentParser(prog="insurance_schedule_cli")
|
|
372
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
373
|
+
|
|
374
|
+
a = sub.add_parser("renewal-query")
|
|
375
|
+
a.add_argument("--days", required=True)
|
|
376
|
+
a.set_defaults(func=cmd_renewal_query)
|
|
377
|
+
|
|
378
|
+
a = sub.add_parser("renewal-remind")
|
|
379
|
+
a.add_argument("--days", required=True)
|
|
380
|
+
a.add_argument("--mark-sent", action="store_true")
|
|
381
|
+
a.set_defaults(func=cmd_renewal_remind)
|
|
382
|
+
|
|
383
|
+
a = sub.add_parser("followup-check")
|
|
384
|
+
a.add_argument("--days", required=True)
|
|
385
|
+
a.set_defaults(func=cmd_followup_check)
|
|
386
|
+
|
|
387
|
+
a = sub.add_parser("followup-remind")
|
|
388
|
+
a.add_argument("--days", required=True)
|
|
389
|
+
a.add_argument("--mark-sent", action="store_true")
|
|
390
|
+
a.set_defaults(func=cmd_followup_remind)
|
|
391
|
+
|
|
392
|
+
a = sub.add_parser("dormant-query")
|
|
393
|
+
a.add_argument("--days", default="60")
|
|
394
|
+
a.set_defaults(func=cmd_dormant_query)
|
|
395
|
+
|
|
396
|
+
a = sub.add_parser("dormant-wake")
|
|
397
|
+
a.add_argument("--days", default="60")
|
|
398
|
+
a.set_defaults(func=cmd_dormant_wake)
|
|
399
|
+
|
|
400
|
+
a = sub.add_parser("special-date-query")
|
|
401
|
+
a.add_argument("--type", required=True, choices=["birthday", "anniversary"])
|
|
402
|
+
a.add_argument("--month")
|
|
403
|
+
a.set_defaults(func=cmd_special_date_query)
|
|
404
|
+
|
|
405
|
+
a = sub.add_parser("daily-summary")
|
|
406
|
+
a.set_defaults(func=cmd_daily_summary)
|
|
407
|
+
|
|
408
|
+
a = sub.add_parser("dashboard")
|
|
409
|
+
a.add_argument("--json", action="store_true")
|
|
410
|
+
a.set_defaults(func=cmd_dashboard)
|
|
411
|
+
|
|
412
|
+
return p
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def main(argv: list[str] | None = None) -> int:
|
|
416
|
+
parser = build_parser()
|
|
417
|
+
args = parser.parse_args(argv)
|
|
418
|
+
conn = db.connect()
|
|
419
|
+
try:
|
|
420
|
+
return int(args.func(conn, args) or 0)
|
|
421
|
+
except sqlite3.Error as e:
|
|
422
|
+
print_json({"ok": False, "error": "sqlite_error", "hint": str(e)})
|
|
423
|
+
return 1
|
|
424
|
+
finally:
|
|
425
|
+
conn.close()
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
if __name__ == "__main__":
|
|
429
|
+
raise SystemExit(main())
|