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.
Files changed (78) hide show
  1. package/agents/parent-toddler/.config.json +7 -1
  2. package/agents/parent-toddler/HEARTBEAT.md +4 -4
  3. package/agents/parent-toddler/TOOLS.md +9 -0
  4. package/agents/parent-toddler/scripts/compact_sessions_over_threshold.py +368 -0
  5. package/package.json +1 -1
  6. package/skills/agent-install/src/SKILL.md +2 -0
  7. package/skills/agent-install/src/scripts/common.py +20 -0
  8. package/skills/agent-install/src/scripts/update_openclaw.py +5 -0
  9. package/skills/image-classify/skill.json +12 -5
  10. package/skills/image-classify/src/SKILL.md +1 -1
  11. package/skills/image-classify/src/references/config.json +1 -1
  12. package/skills/insurance-customer-policy/skill.json +12 -0
  13. package/skills/insurance-customer-policy/src/SKILL.md +121 -0
  14. package/skills/insurance-customer-policy/src/pyproject.toml +9 -0
  15. package/skills/insurance-customer-policy/src/scripts/cli.py +16 -0
  16. package/skills/insurance-customer-policy/src/scripts/cloud_insurance_full_test.py +785 -0
  17. package/skills/insurance-customer-policy/src/scripts/dashboard_all.py +205 -0
  18. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__init__.py +0 -0
  19. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__main__.py +4 -0
  20. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/cli.py +816 -0
  21. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/db.py +181 -0
  22. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/insurance_paths.py +184 -0
  23. package/skills/insurance-customer-policy/src/scripts/run_e2e_smoke.py +164 -0
  24. package/skills/insurance-customer-policy/src/scripts/test_cloud_zero_config.py +217 -0
  25. package/skills/insurance-customer-policy/src/scripts/test_dashboard_all.py +113 -0
  26. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__init__.py +0 -0
  27. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__main__.py +4 -0
  28. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/cli.py +816 -0
  29. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/db.py +181 -0
  30. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/insurance_paths.py +184 -0
  31. package/skills/insurance-product-analysis/skill.json +12 -0
  32. package/skills/insurance-product-analysis/src/SKILL.md +99 -0
  33. package/skills/insurance-product-analysis/src/pyproject.toml +9 -0
  34. package/skills/insurance-product-analysis/src/scripts/cli.py +16 -0
  35. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__init__.py +0 -0
  36. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__main__.py +4 -0
  37. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/cli.py +545 -0
  38. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/db.py +180 -0
  39. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/orchestrator.py +163 -0
  40. package/skills/insurance-product-analysis/src/scripts/run_e2e_smoke.py +202 -0
  41. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__init__.py +0 -0
  42. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__main__.py +4 -0
  43. package/skills/insurance-product-analysis/src/src/insurance_product_cli/cli.py +545 -0
  44. package/skills/insurance-product-analysis/src/src/insurance_product_cli/db.py +180 -0
  45. package/skills/insurance-product-analysis/src/src/insurance_product_cli/orchestrator.py +163 -0
  46. package/skills/insurance-sales-pipeline/skill.json +12 -0
  47. package/skills/insurance-sales-pipeline/src/SKILL.md +102 -0
  48. package/skills/insurance-sales-pipeline/src/pyproject.toml +9 -0
  49. package/skills/insurance-sales-pipeline/src/scripts/cli.py +16 -0
  50. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__init__.py +0 -0
  51. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__main__.py +4 -0
  52. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/cli.py +496 -0
  53. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/db.py +180 -0
  54. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/orchestrator.py +36 -0
  55. package/skills/insurance-sales-pipeline/src/scripts/run_e2e_smoke.py +208 -0
  56. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__init__.py +0 -0
  57. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__main__.py +4 -0
  58. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/cli.py +496 -0
  59. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/db.py +180 -0
  60. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/orchestrator.py +36 -0
  61. package/skills/insurance-schedule-renewal/skill.json +12 -0
  62. package/skills/insurance-schedule-renewal/src/SKILL.md +94 -0
  63. package/skills/insurance-schedule-renewal/src/pyproject.toml +9 -0
  64. package/skills/insurance-schedule-renewal/src/scripts/cli.py +16 -0
  65. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__init__.py +0 -0
  66. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__main__.py +4 -0
  67. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/cli.py +429 -0
  68. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/db.py +180 -0
  69. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/orchestrator.py +94 -0
  70. package/skills/insurance-schedule-renewal/src/scripts/run_e2e_smoke.py +218 -0
  71. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__init__.py +0 -0
  72. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__main__.py +4 -0
  73. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/cli.py +429 -0
  74. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/db.py +180 -0
  75. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/orchestrator.py +94 -0
  76. package/skills/insurance-shared-data/skill.json +20 -0
  77. package/skills/insurance-shared-data/src/SKILL.md +33 -0
  78. 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,9 @@
1
+ [project]
2
+ name = "insurance-schedule-renewal"
3
+ version = "1.0.0"
4
+ description = "Insurance schedule & renewal reminder CLI"
5
+ requires-python = ">=3.10"
6
+ dependencies = []
7
+
8
+ [tool.uv]
9
+ package = true
@@ -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:]))
@@ -0,0 +1,4 @@
1
+ from insurance_schedule_cli.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -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())