sophhub 0.4.21 → 0.4.23
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/agents/parent-toddler/.config.json +7 -1
- package/agents/parent-toddler/HEARTBEAT.md +4 -4
- package/agents/parent-toddler/TOOLS.md +9 -0
- package/agents/parent-toddler/scripts/compact_sessions_over_threshold.py +368 -0
- package/package.json +1 -1
- package/skills/agent-install/src/SKILL.md +2 -0
- package/skills/agent-install/src/scripts/common.py +20 -0
- package/skills/agent-install/src/scripts/update_openclaw.py +5 -0
- package/skills/image-classify/skill.json +12 -5
- package/skills/image-classify/src/SKILL.md +1 -1
- package/skills/image-classify/src/references/config.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,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())
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Shared SQLite schema for insurance skills."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sqlite3
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def default_db_path() -> Path:
|
|
10
|
+
base = Path.home() / ".config" / "insurance"
|
|
11
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
return base / "insurance.sqlite3"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_db_path() -> Path:
|
|
16
|
+
for key in ("INSURANCE_DB_PATH", "INSURANCE_SCHEDULE_DB_PATH"):
|
|
17
|
+
p = os.environ.get(key)
|
|
18
|
+
if p:
|
|
19
|
+
path = Path(p)
|
|
20
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
return path
|
|
22
|
+
return default_db_path()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def connect() -> sqlite3.Connection:
|
|
26
|
+
conn = sqlite3.connect(str(get_db_path()))
|
|
27
|
+
conn.row_factory = sqlite3.Row
|
|
28
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
29
|
+
init_schema(conn)
|
|
30
|
+
return conn
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def init_schema(conn: sqlite3.Connection) -> None:
|
|
34
|
+
conn.executescript(
|
|
35
|
+
"""
|
|
36
|
+
CREATE TABLE IF NOT EXISTS customers (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
name TEXT NOT NULL,
|
|
39
|
+
phone TEXT,
|
|
40
|
+
age INTEGER,
|
|
41
|
+
gender TEXT,
|
|
42
|
+
income REAL,
|
|
43
|
+
household_json TEXT NOT NULL DEFAULT '{}',
|
|
44
|
+
occupation TEXT,
|
|
45
|
+
birthday TEXT,
|
|
46
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
47
|
+
fields_json TEXT NOT NULL DEFAULT '{}',
|
|
48
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
49
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
50
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_customers_phone ON customers(phone);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_customers_birthday ON customers(birthday);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS policies (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
|
|
58
|
+
company TEXT NOT NULL,
|
|
59
|
+
product_name TEXT NOT NULL,
|
|
60
|
+
product_type TEXT NOT NULL,
|
|
61
|
+
coverage REAL NOT NULL DEFAULT 0,
|
|
62
|
+
premium REAL NOT NULL DEFAULT 0,
|
|
63
|
+
pay_period TEXT,
|
|
64
|
+
effective_date TEXT NOT NULL,
|
|
65
|
+
expire_date TEXT,
|
|
66
|
+
next_pay_date TEXT,
|
|
67
|
+
insured_name TEXT,
|
|
68
|
+
beneficiary TEXT,
|
|
69
|
+
notes TEXT,
|
|
70
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
71
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
72
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
73
|
+
);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_policies_customer ON policies(customer_id, status);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_policies_expire ON policies(expire_date, status);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_policies_next_pay ON policies(next_pay_date, status);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_policies_type ON policies(product_type, status);
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS followups (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
|
82
|
+
content TEXT NOT NULL,
|
|
83
|
+
next_step TEXT,
|
|
84
|
+
next_date TEXT,
|
|
85
|
+
channel TEXT,
|
|
86
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
87
|
+
);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_followups_customer ON followups(customer_id, created_at DESC);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS customer_fields (
|
|
91
|
+
name TEXT PRIMARY KEY,
|
|
92
|
+
type TEXT NOT NULL DEFAULT 'text'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
CREATE TABLE IF NOT EXISTS products (
|
|
96
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
97
|
+
name TEXT NOT NULL UNIQUE,
|
|
98
|
+
company TEXT NOT NULL,
|
|
99
|
+
product_type TEXT NOT NULL,
|
|
100
|
+
coverage_min REAL,
|
|
101
|
+
coverage_max REAL,
|
|
102
|
+
premium_min REAL,
|
|
103
|
+
premium_max REAL,
|
|
104
|
+
age_min INTEGER,
|
|
105
|
+
age_max INTEGER,
|
|
106
|
+
waiting_period TEXT,
|
|
107
|
+
deductible TEXT,
|
|
108
|
+
renewal_terms TEXT,
|
|
109
|
+
highlights_json TEXT NOT NULL DEFAULT '[]',
|
|
110
|
+
target_audience TEXT,
|
|
111
|
+
file_path TEXT,
|
|
112
|
+
raw_text TEXT,
|
|
113
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
114
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
115
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
116
|
+
);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_products_type ON products(product_type, status);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_products_company ON products(company, status);
|
|
119
|
+
|
|
120
|
+
CREATE TABLE IF NOT EXISTS product_analysis_cache (
|
|
121
|
+
product_id INTEGER PRIMARY KEY REFERENCES products(id) ON DELETE CASCADE,
|
|
122
|
+
key_terms_json TEXT,
|
|
123
|
+
selling_points_json TEXT,
|
|
124
|
+
fit_audience_json TEXT,
|
|
125
|
+
pitch_script TEXT,
|
|
126
|
+
analyzed_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
CREATE TABLE IF NOT EXISTS deals (
|
|
130
|
+
id TEXT PRIMARY KEY,
|
|
131
|
+
customer_id TEXT NOT NULL,
|
|
132
|
+
customer_name TEXT,
|
|
133
|
+
product_name TEXT,
|
|
134
|
+
product_type TEXT,
|
|
135
|
+
stage TEXT NOT NULL,
|
|
136
|
+
estimated_premium REAL NOT NULL DEFAULT 0,
|
|
137
|
+
actual_premium REAL,
|
|
138
|
+
expected_close_date TEXT,
|
|
139
|
+
closed_at TEXT,
|
|
140
|
+
lost_reason TEXT,
|
|
141
|
+
lost_note TEXT,
|
|
142
|
+
note TEXT,
|
|
143
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
144
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
145
|
+
);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_deals_stage ON deals(stage, expected_close_date);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_deals_customer ON deals(customer_id);
|
|
148
|
+
|
|
149
|
+
CREATE TABLE IF NOT EXISTS deal_stage_log (
|
|
150
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
151
|
+
deal_id TEXT NOT NULL REFERENCES deals(id) ON DELETE CASCADE,
|
|
152
|
+
from_stage TEXT,
|
|
153
|
+
to_stage TEXT NOT NULL,
|
|
154
|
+
note TEXT,
|
|
155
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
156
|
+
);
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_stage_log_deal ON deal_stage_log(deal_id, created_at);
|
|
158
|
+
|
|
159
|
+
CREATE TABLE IF NOT EXISTS monthly_targets (
|
|
160
|
+
month TEXT PRIMARY KEY,
|
|
161
|
+
target_premium REAL NOT NULL,
|
|
162
|
+
note TEXT,
|
|
163
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
164
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
CREATE TABLE IF NOT EXISTS sent_reminders (
|
|
168
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
169
|
+
reminder_type TEXT NOT NULL,
|
|
170
|
+
target_id TEXT NOT NULL,
|
|
171
|
+
bucket TEXT NOT NULL,
|
|
172
|
+
sent_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
173
|
+
status TEXT NOT NULL DEFAULT 'sent',
|
|
174
|
+
note TEXT,
|
|
175
|
+
UNIQUE (reminder_type, target_id, bucket)
|
|
176
|
+
);
|
|
177
|
+
CREATE INDEX IF NOT EXISTS idx_sent_target ON sent_reminders(target_id, reminder_type);
|
|
178
|
+
"""
|
|
179
|
+
)
|
|
180
|
+
conn.commit()
|