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,496 @@
1
+ """Insurance sales pipeline CLI."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import re
7
+ import sqlite3
8
+ import sys
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ from insurance_pipeline_cli import db, orchestrator
13
+
14
+ try:
15
+ sys.stdout.reconfigure(encoding="utf-8")
16
+ sys.stderr.reconfigure(encoding="utf-8")
17
+ except (AttributeError, OSError):
18
+ pass
19
+
20
+
21
+ def print_json(obj: Any) -> None:
22
+ print(json.dumps(obj, ensure_ascii=False, indent=2))
23
+
24
+
25
+ def next_id(conn: sqlite3.Connection, prefix: str, table: str) -> str:
26
+ rows = conn.execute(
27
+ f"SELECT id FROM {table} WHERE id GLOB ?", (f"{prefix}[0-9]*",)
28
+ ).fetchall()
29
+ max_n = 0
30
+ for r in rows:
31
+ m = re.match(rf"^{prefix}(\d+)$", r["id"])
32
+ if m:
33
+ max_n = max(max_n, int(m.group(1)))
34
+ return f"{prefix}{max_n + 1:03d}"
35
+
36
+
37
+ STAGES = ("在谈", "方案中", "议价", "已成交", "已流失")
38
+ STAGE_PROBABILITY = {"在谈": 0.20, "方案中": 0.50, "议价": 0.80, "已成交": 1.0, "已流失": 0.0}
39
+ ACTIVE_STAGES = ("在谈", "方案中", "议价")
40
+ LOST_REASONS = ("价格", "条款", "竞品", "自身", "其他")
41
+
42
+
43
+ # ---------- deal CRUD ----------
44
+
45
+ def cmd_deal_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
46
+ if not orchestrator.customer_exists(args.customer_id):
47
+ print_json({"ok": False, "error": "customer_not_found",
48
+ "hint": "请先在 insurance-customer-policy 录入该客户"})
49
+ return 1
50
+ customer_name = ""
51
+ cd = orchestrator.customer_query(args.customer_id)
52
+ if cd.get("ok") and cd.get("customers"):
53
+ customer_name = cd["customers"][0].get("name") or ""
54
+ if args.product:
55
+ pd = orchestrator.product_query(args.product)
56
+ if not (pd.get("ok") and pd.get("products")):
57
+ print_json({"ok": False, "error": "product_not_found",
58
+ "hint": "请先在 insurance-product-analysis 录入该产品",
59
+ "product": args.product})
60
+ return 1
61
+ stage = args.stage or "在谈"
62
+ if stage not in STAGES:
63
+ print_json({"ok": False, "error": "bad_stage", "stages": list(STAGES)})
64
+ return 1
65
+ did = args.deal_id or next_id(conn, "D", "deals")
66
+ conn.execute(
67
+ """INSERT INTO deals
68
+ (id, customer_id, customer_name, product_name, product_type,
69
+ stage, estimated_premium, expected_close_date, note)
70
+ VALUES (?,?,?,?,?,?,?,?,?)""",
71
+ (
72
+ did,
73
+ args.customer_id,
74
+ customer_name,
75
+ args.product or "",
76
+ args.product_type or "",
77
+ stage,
78
+ float(args.estimated_premium or 0),
79
+ args.expected_close_date or "",
80
+ args.note or "",
81
+ ),
82
+ )
83
+ conn.execute(
84
+ "INSERT INTO deal_stage_log (deal_id, from_stage, to_stage, note) VALUES (?,?,?,?)",
85
+ (did, None, stage, "deal-add"),
86
+ )
87
+ conn.commit()
88
+ print_json({"ok": True, "id": did, "customer_id": args.customer_id, "stage": stage})
89
+ return 0
90
+
91
+
92
+ def cmd_deal_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
93
+ cur = conn.cursor()
94
+ if args.deal_id:
95
+ rows = list(cur.execute("SELECT * FROM deals WHERE id=?", (args.deal_id,)))
96
+ elif args.customer_id:
97
+ rows = list(cur.execute("SELECT * FROM deals WHERE customer_id=? ORDER BY created_at DESC",
98
+ (args.customer_id,)))
99
+ elif args.stage:
100
+ rows = list(cur.execute("SELECT * FROM deals WHERE stage=? ORDER BY created_at DESC", (args.stage,)))
101
+ elif args.status == "active":
102
+ placeholders = ",".join("?" * len(ACTIVE_STAGES))
103
+ rows = list(cur.execute(
104
+ f"SELECT * FROM deals WHERE stage IN ({placeholders}) ORDER BY created_at DESC",
105
+ ACTIVE_STAGES,
106
+ ))
107
+ else:
108
+ rows = list(cur.execute("SELECT * FROM deals ORDER BY created_at DESC"))
109
+ print_json({"ok": True, "deals": [dict(r) for r in rows], "count": len(rows)})
110
+ return 0
111
+
112
+
113
+ _DEAL_UPDATE_FIELDS = {
114
+ "customer_id": "customer_id",
115
+ "product_name": "product_name",
116
+ "product_type": "product_type",
117
+ "estimated_premium": "estimated_premium",
118
+ "actual_premium": "actual_premium",
119
+ "expected_close_date": "expected_close_date",
120
+ "note": "note",
121
+ }
122
+
123
+
124
+ def cmd_deal_update(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
125
+ try:
126
+ payload = json.loads(args.set)
127
+ except json.JSONDecodeError as e:
128
+ print_json({"ok": False, "error": "bad_json", "hint": str(e)})
129
+ return 1
130
+ row = conn.execute("SELECT id FROM deals WHERE id=?", (args.deal_id,)).fetchone()
131
+ if not row:
132
+ print_json({"ok": False, "error": "deal_not_found"})
133
+ return 1
134
+ sets, vals = [], []
135
+ for k, v in payload.items():
136
+ col = _DEAL_UPDATE_FIELDS.get(k)
137
+ if col is not None:
138
+ sets.append(f"{col}=?")
139
+ vals.append(v)
140
+ if sets:
141
+ sets.append("updated_at=datetime('now','localtime')")
142
+ vals.append(args.deal_id)
143
+ conn.execute(f"UPDATE deals SET {', '.join(sets)} WHERE id=?", vals)
144
+ conn.commit()
145
+ print_json({"ok": True, "id": args.deal_id})
146
+ return 0
147
+
148
+
149
+ def cmd_deal_delete(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
150
+ row = conn.execute("SELECT id FROM deals WHERE id=?", (args.deal_id,)).fetchone()
151
+ if not row:
152
+ print_json({"ok": False, "error": "deal_not_found"})
153
+ return 1
154
+ if not args.yes:
155
+ print_json({"ok": False, "error": "need_confirm", "hint": "请加 --yes 确认删除"})
156
+ return 1
157
+ conn.execute("DELETE FROM deals WHERE id=?", (args.deal_id,))
158
+ conn.commit()
159
+ print_json({"ok": True})
160
+ return 0
161
+
162
+
163
+ # ---------- deal-stage ----------
164
+
165
+ def cmd_deal_stage(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
166
+ if args.to not in STAGES:
167
+ print_json({"ok": False, "error": "bad_stage", "stages": list(STAGES)})
168
+ return 1
169
+ row = conn.execute("SELECT * FROM deals WHERE id=?", (args.deal_id,)).fetchone()
170
+ if not row:
171
+ print_json({"ok": False, "error": "deal_not_found"})
172
+ return 1
173
+ from_stage = row["stage"]
174
+ sets = ["stage=?", "updated_at=datetime('now','localtime')"]
175
+ vals: list[Any] = [args.to]
176
+ if args.to == "已成交":
177
+ sets.append("closed_at=datetime('now','localtime')")
178
+ if args.actual_premium is not None:
179
+ sets.append("actual_premium=?")
180
+ vals.append(float(args.actual_premium))
181
+ if args.to == "已流失":
182
+ sets.append("closed_at=datetime('now','localtime')")
183
+ if args.lost_reason:
184
+ sets.append("lost_reason=?")
185
+ vals.append(args.lost_reason)
186
+ if args.lost_note:
187
+ sets.append("lost_note=?")
188
+ vals.append(args.lost_note)
189
+ vals.append(args.deal_id)
190
+ conn.execute(f"UPDATE deals SET {', '.join(sets)} WHERE id=?", vals)
191
+ conn.execute(
192
+ "INSERT INTO deal_stage_log (deal_id, from_stage, to_stage, note) VALUES (?,?,?,?)",
193
+ (args.deal_id, from_stage, args.to, args.note or ""),
194
+ )
195
+ conn.commit()
196
+ print_json({"ok": True, "id": args.deal_id, "from": from_stage, "to": args.to})
197
+ return 0
198
+
199
+
200
+ # ---------- deal-forecast ----------
201
+
202
+ def _month_bounds(month: str) -> tuple[str, str]:
203
+ """month = 'YYYY-MM' → ('YYYY-MM-01 00:00:00', 'YYYY-(MM+1)-01 00:00:00')."""
204
+ y, m = map(int, month.split("-"))
205
+ start = f"{y:04d}-{m:02d}-01 00:00:00"
206
+ if m == 12:
207
+ end = f"{y + 1:04d}-01-01 00:00:00"
208
+ else:
209
+ end = f"{y:04d}-{m + 1:02d}-01 00:00:00"
210
+ return start, end
211
+
212
+
213
+ def cmd_deal_forecast(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
214
+ month = args.month or datetime.now().strftime("%Y-%m")
215
+ cur = conn.cursor()
216
+ by_stage: dict[str, dict] = {}
217
+ by_type: dict[str, float] = {}
218
+ total_forecast = 0.0
219
+ closed = 0.0
220
+ rows = list(cur.execute("SELECT * FROM deals"))
221
+ for r in rows:
222
+ if r["stage"] == "已成交":
223
+ if r["closed_at"]:
224
+ start, end = _month_bounds(month)
225
+ if start <= r["closed_at"] < end:
226
+ closed += float(r["actual_premium"] or r["estimated_premium"] or 0)
227
+ elif r["stage"] in ACTIVE_STAGES:
228
+ prob = STAGE_PROBABILITY[r["stage"]]
229
+ est = float(r["estimated_premium"] or 0)
230
+ forecast = prob * est
231
+ total_forecast += forecast
232
+ slot = by_stage.setdefault(r["stage"], {"count": 0, "estimated": 0.0, "forecast": 0.0})
233
+ slot["count"] += 1
234
+ slot["estimated"] += est
235
+ slot["forecast"] += forecast
236
+ ptype = r["product_type"] or "其他"
237
+ by_type[ptype] = by_type.get(ptype, 0.0) + forecast
238
+ target_row = conn.execute(
239
+ "SELECT target_premium FROM monthly_targets WHERE month=?", (month,)
240
+ ).fetchone()
241
+ target = float(target_row["target_premium"]) if target_row else 0.0
242
+ print_json({
243
+ "ok": True,
244
+ "month": month,
245
+ "by_stage": by_stage,
246
+ "by_type": by_type,
247
+ "total_forecast": round(total_forecast, 2),
248
+ "closed": round(closed, 2),
249
+ "target": target,
250
+ "gap_to_target": round(max(0.0, target - closed - total_forecast), 2),
251
+ })
252
+ return 0
253
+
254
+
255
+ # ---------- target-track ----------
256
+
257
+ def _days_remaining_in_month(month: str) -> int:
258
+ y, m = map(int, month.split("-"))
259
+ if m == 12:
260
+ next_first = datetime(y + 1, 1, 1)
261
+ else:
262
+ next_first = datetime(y, m + 1, 1)
263
+ today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
264
+ if today >= next_first:
265
+ return 0
266
+ return max(0, (next_first - today).days)
267
+
268
+
269
+ def cmd_target_track(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
270
+ month = args.month or datetime.now().strftime("%Y-%m")
271
+ if args.target is not None:
272
+ conn.execute(
273
+ """INSERT INTO monthly_targets (month, target_premium, note)
274
+ VALUES (?,?,?)
275
+ ON CONFLICT(month) DO UPDATE SET
276
+ target_premium=excluded.target_premium,
277
+ note=excluded.note,
278
+ updated_at=datetime('now','localtime')""",
279
+ (month, float(args.target), args.note or ""),
280
+ )
281
+ conn.commit()
282
+ target_row = conn.execute(
283
+ "SELECT target_premium, note FROM monthly_targets WHERE month=?", (month,)
284
+ ).fetchone()
285
+ target = float(target_row["target_premium"]) if target_row else 0.0
286
+ target_note = target_row["note"] if target_row else ""
287
+ start, end = _month_bounds(month)
288
+ closed = float(conn.execute(
289
+ """SELECT COALESCE(SUM(COALESCE(actual_premium, estimated_premium, 0)),0) AS s
290
+ FROM deals WHERE stage='已成交' AND closed_at >= ? AND closed_at < ?""",
291
+ (start, end),
292
+ ).fetchone()["s"])
293
+ forecast = 0.0
294
+ forecast_breakdown: dict[str, float] = {}
295
+ for stage in ACTIVE_STAGES:
296
+ s_total = float(conn.execute(
297
+ "SELECT COALESCE(SUM(estimated_premium),0) AS s FROM deals WHERE stage=?", (stage,),
298
+ ).fetchone()["s"])
299
+ forecast_breakdown[stage] = round(s_total * STAGE_PROBABILITY[stage], 2)
300
+ forecast += s_total * STAGE_PROBABILITY[stage]
301
+ gap = max(0.0, target - closed)
302
+ days_left = _days_remaining_in_month(month)
303
+ progress = (closed / target * 100) if target > 0 else 0.0
304
+ bar_filled = int(progress / 100 * 16)
305
+ bar = "█" * max(0, min(16, bar_filled)) + "░" * max(0, 16 - max(0, min(16, bar_filled)))
306
+ md = (
307
+ f"🎯 **{month} 目标跟踪**\n\n"
308
+ f"目标:¥{target:,.0f}\n"
309
+ f"已完成:¥{closed:,.0f}({progress:.0f}%)\n"
310
+ f"在谈预估:¥{forecast:,.0f}\n"
311
+ f"差距:¥{gap:,.0f}\n\n"
312
+ f"⏰ 本月剩余:{days_left} 天\n"
313
+ f"📊 完成进度:{bar} {progress:.0f}%"
314
+ )
315
+ print_json({
316
+ "ok": True,
317
+ "month": month,
318
+ "target": target,
319
+ "target_note": target_note,
320
+ "closed": round(closed, 2),
321
+ "forecast": round(forecast, 2),
322
+ "forecast_breakdown": forecast_breakdown,
323
+ "gap": round(gap, 2),
324
+ "progress_pct": round(progress, 2),
325
+ "days_remaining": days_left,
326
+ "markdown": md,
327
+ })
328
+ return 0
329
+
330
+
331
+ # ---------- deal-lost ----------
332
+
333
+ def cmd_deal_lost(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
334
+ if args.stats:
335
+ month = args.month or datetime.now().strftime("%Y-%m")
336
+ start, end = _month_bounds(month)
337
+ rows = list(conn.execute(
338
+ """SELECT lost_reason, COUNT(*) AS n, COALESCE(SUM(estimated_premium),0) AS s
339
+ FROM deals WHERE stage='已流失' AND closed_at >= ? AND closed_at < ?
340
+ GROUP BY lost_reason ORDER BY n DESC""",
341
+ (start, end),
342
+ ))
343
+ total_n = sum(r["n"] for r in rows)
344
+ total_premium = sum(float(r["s"]) for r in rows)
345
+ breakdown = [{"reason": (r["lost_reason"] or "其他"),
346
+ "count": int(r["n"]),
347
+ "premium": float(r["s"])} for r in rows]
348
+ print_json({"ok": True, "month": month, "total_count": total_n,
349
+ "total_premium": round(total_premium, 2),
350
+ "by_reason": breakdown})
351
+ return 0
352
+ if not args.deal_id:
353
+ print_json({"ok": False, "error": "missing_args", "hint": "请提供 --deal-id 或 --stats"})
354
+ return 1
355
+ if args.reason and args.reason not in LOST_REASONS:
356
+ print_json({"ok": False, "error": "bad_reason", "reasons": list(LOST_REASONS)})
357
+ return 1
358
+ row = conn.execute("SELECT * FROM deals WHERE id=?", (args.deal_id,)).fetchone()
359
+ if not row:
360
+ print_json({"ok": False, "error": "deal_not_found"})
361
+ return 1
362
+ from_stage = row["stage"]
363
+ conn.execute(
364
+ """UPDATE deals SET stage='已流失', lost_reason=?, lost_note=?,
365
+ closed_at=datetime('now','localtime'),
366
+ updated_at=datetime('now','localtime') WHERE id=?""",
367
+ (args.reason or "其他", args.note or "", args.deal_id),
368
+ )
369
+ conn.execute(
370
+ "INSERT INTO deal_stage_log (deal_id, from_stage, to_stage, note) VALUES (?,?,?,?)",
371
+ (args.deal_id, from_stage, "已流失", args.note or ""),
372
+ )
373
+ conn.commit()
374
+ print_json({"ok": True, "id": args.deal_id, "from": from_stage, "to": "已流失"})
375
+ return 0
376
+
377
+
378
+ # ---------- dashboard ----------
379
+
380
+ def cmd_dashboard(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
381
+ month = datetime.now().strftime("%Y-%m")
382
+ start, end = _month_bounds(month)
383
+ placeholders = ",".join("?" * len(ACTIVE_STAGES))
384
+ active = int(conn.execute(
385
+ f"SELECT COUNT(*) AS n FROM deals WHERE stage IN ({placeholders})", ACTIVE_STAGES,
386
+ ).fetchone()["n"])
387
+ closed = float(conn.execute(
388
+ """SELECT COALESCE(SUM(COALESCE(actual_premium, estimated_premium, 0)),0) AS s
389
+ FROM deals WHERE stage='已成交' AND closed_at >= ? AND closed_at < ?""",
390
+ (start, end),
391
+ ).fetchone()["s"])
392
+ forecast = 0.0
393
+ for stage in ACTIVE_STAGES:
394
+ s_total = float(conn.execute(
395
+ "SELECT COALESCE(SUM(estimated_premium),0) AS s FROM deals WHERE stage=?", (stage,),
396
+ ).fetchone()["s"])
397
+ forecast += s_total * STAGE_PROBABILITY[stage]
398
+ target_row = conn.execute(
399
+ "SELECT target_premium FROM monthly_targets WHERE month=?", (month,)
400
+ ).fetchone()
401
+ target = float(target_row["target_premium"]) if target_row else 0.0
402
+ print_json({
403
+ "ok": True,
404
+ "skill": "insurance-sales-pipeline",
405
+ "this_month": month,
406
+ "active_deals": active,
407
+ "this_month_target": target,
408
+ "this_month_closed": round(closed, 2),
409
+ "this_month_forecast": round(forecast, 2),
410
+ })
411
+ return 0
412
+
413
+
414
+ # ---------- argparse ----------
415
+
416
+ def build_parser() -> argparse.ArgumentParser:
417
+ p = argparse.ArgumentParser(prog="insurance_pipeline_cli")
418
+ sub = p.add_subparsers(dest="cmd", required=True)
419
+
420
+ a = sub.add_parser("deal-add")
421
+ a.add_argument("--customer-id", required=True)
422
+ a.add_argument("--deal-id")
423
+ a.add_argument("--product")
424
+ a.add_argument("--product-type")
425
+ a.add_argument("--stage")
426
+ a.add_argument("--estimated-premium", type=float)
427
+ a.add_argument("--expected-close-date")
428
+ a.add_argument("--note")
429
+ a.set_defaults(func=cmd_deal_add)
430
+
431
+ a = sub.add_parser("deal-query")
432
+ a.add_argument("--deal-id")
433
+ a.add_argument("--customer-id")
434
+ a.add_argument("--stage")
435
+ a.add_argument("--status", choices=["active"])
436
+ a.set_defaults(func=cmd_deal_query)
437
+
438
+ a = sub.add_parser("deal-update")
439
+ a.add_argument("--deal-id", required=True)
440
+ a.add_argument("--set", required=True)
441
+ a.set_defaults(func=cmd_deal_update)
442
+
443
+ a = sub.add_parser("deal-delete")
444
+ a.add_argument("--deal-id", required=True)
445
+ a.add_argument("--yes", action="store_true")
446
+ a.set_defaults(func=cmd_deal_delete)
447
+
448
+ a = sub.add_parser("deal-stage")
449
+ a.add_argument("--deal-id", required=True)
450
+ a.add_argument("--to", required=True)
451
+ a.add_argument("--note")
452
+ a.add_argument("--actual-premium", type=float)
453
+ a.add_argument("--lost-reason")
454
+ a.add_argument("--lost-note")
455
+ a.set_defaults(func=cmd_deal_stage)
456
+
457
+ a = sub.add_parser("deal-forecast")
458
+ a.add_argument("--month")
459
+ a.set_defaults(func=cmd_deal_forecast)
460
+
461
+ a = sub.add_parser("target-track")
462
+ a.add_argument("--month")
463
+ a.add_argument("--target", type=float)
464
+ a.add_argument("--note")
465
+ a.set_defaults(func=cmd_target_track)
466
+
467
+ a = sub.add_parser("deal-lost")
468
+ a.add_argument("--deal-id")
469
+ a.add_argument("--reason")
470
+ a.add_argument("--note")
471
+ a.add_argument("--stats", action="store_true")
472
+ a.add_argument("--month")
473
+ a.set_defaults(func=cmd_deal_lost)
474
+
475
+ a = sub.add_parser("dashboard")
476
+ a.add_argument("--json", action="store_true")
477
+ a.set_defaults(func=cmd_dashboard)
478
+
479
+ return p
480
+
481
+
482
+ def main(argv: list[str] | None = None) -> int:
483
+ parser = build_parser()
484
+ args = parser.parse_args(argv)
485
+ conn = db.connect()
486
+ try:
487
+ return int(args.func(conn, args) or 0)
488
+ except sqlite3.Error as e:
489
+ print_json({"ok": False, "error": "sqlite_error", "hint": str(e)})
490
+ return 1
491
+ finally:
492
+ conn.close()
493
+
494
+
495
+ if __name__ == "__main__":
496
+ 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_PIPELINE_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()