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,181 @@
|
|
|
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
|
+
# 新统一变量优先,兼容旧变量。
|
|
17
|
+
for key in ("INSURANCE_DB_PATH", "INSURANCE_CUSTOMER_DB_PATH"):
|
|
18
|
+
p = os.environ.get(key)
|
|
19
|
+
if p:
|
|
20
|
+
path = Path(p)
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
return path
|
|
23
|
+
return default_db_path()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def connect() -> sqlite3.Connection:
|
|
27
|
+
conn = sqlite3.connect(str(get_db_path()))
|
|
28
|
+
conn.row_factory = sqlite3.Row
|
|
29
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
30
|
+
init_schema(conn)
|
|
31
|
+
return conn
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def init_schema(conn: sqlite3.Connection) -> None:
|
|
35
|
+
conn.executescript(
|
|
36
|
+
"""
|
|
37
|
+
CREATE TABLE IF NOT EXISTS customers (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
name TEXT NOT NULL,
|
|
40
|
+
phone TEXT,
|
|
41
|
+
age INTEGER,
|
|
42
|
+
gender TEXT,
|
|
43
|
+
income REAL,
|
|
44
|
+
household_json TEXT NOT NULL DEFAULT '{}',
|
|
45
|
+
occupation TEXT,
|
|
46
|
+
birthday TEXT,
|
|
47
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
48
|
+
fields_json TEXT NOT NULL DEFAULT '{}',
|
|
49
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
50
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
51
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
52
|
+
);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_customers_phone ON customers(phone);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_customers_birthday ON customers(birthday);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS policies (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
|
|
59
|
+
company TEXT NOT NULL,
|
|
60
|
+
product_name TEXT NOT NULL,
|
|
61
|
+
product_type TEXT NOT NULL,
|
|
62
|
+
coverage REAL NOT NULL DEFAULT 0,
|
|
63
|
+
premium REAL NOT NULL DEFAULT 0,
|
|
64
|
+
pay_period TEXT,
|
|
65
|
+
effective_date TEXT NOT NULL,
|
|
66
|
+
expire_date TEXT,
|
|
67
|
+
next_pay_date TEXT,
|
|
68
|
+
insured_name TEXT,
|
|
69
|
+
beneficiary TEXT,
|
|
70
|
+
notes TEXT,
|
|
71
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
72
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
73
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
74
|
+
);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_policies_customer ON policies(customer_id, status);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_policies_expire ON policies(expire_date, status);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_policies_next_pay ON policies(next_pay_date, status);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_policies_type ON policies(product_type, status);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS followups (
|
|
81
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
+
customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
|
83
|
+
content TEXT NOT NULL,
|
|
84
|
+
next_step TEXT,
|
|
85
|
+
next_date TEXT,
|
|
86
|
+
channel TEXT,
|
|
87
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
88
|
+
);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_followups_customer ON followups(customer_id, created_at DESC);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS customer_fields (
|
|
92
|
+
name TEXT PRIMARY KEY,
|
|
93
|
+
type TEXT NOT NULL DEFAULT 'text'
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS products (
|
|
97
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
name TEXT NOT NULL UNIQUE,
|
|
99
|
+
company TEXT NOT NULL,
|
|
100
|
+
product_type TEXT NOT NULL,
|
|
101
|
+
coverage_min REAL,
|
|
102
|
+
coverage_max REAL,
|
|
103
|
+
premium_min REAL,
|
|
104
|
+
premium_max REAL,
|
|
105
|
+
age_min INTEGER,
|
|
106
|
+
age_max INTEGER,
|
|
107
|
+
waiting_period TEXT,
|
|
108
|
+
deductible TEXT,
|
|
109
|
+
renewal_terms TEXT,
|
|
110
|
+
highlights_json TEXT NOT NULL DEFAULT '[]',
|
|
111
|
+
target_audience TEXT,
|
|
112
|
+
file_path TEXT,
|
|
113
|
+
raw_text TEXT,
|
|
114
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
115
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
116
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
117
|
+
);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_products_type ON products(product_type, status);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_products_company ON products(company, status);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS product_analysis_cache (
|
|
122
|
+
product_id INTEGER PRIMARY KEY REFERENCES products(id) ON DELETE CASCADE,
|
|
123
|
+
key_terms_json TEXT,
|
|
124
|
+
selling_points_json TEXT,
|
|
125
|
+
fit_audience_json TEXT,
|
|
126
|
+
pitch_script TEXT,
|
|
127
|
+
analyzed_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
CREATE TABLE IF NOT EXISTS deals (
|
|
131
|
+
id TEXT PRIMARY KEY,
|
|
132
|
+
customer_id TEXT NOT NULL,
|
|
133
|
+
customer_name TEXT,
|
|
134
|
+
product_name TEXT,
|
|
135
|
+
product_type TEXT,
|
|
136
|
+
stage TEXT NOT NULL,
|
|
137
|
+
estimated_premium REAL NOT NULL DEFAULT 0,
|
|
138
|
+
actual_premium REAL,
|
|
139
|
+
expected_close_date TEXT,
|
|
140
|
+
closed_at TEXT,
|
|
141
|
+
lost_reason TEXT,
|
|
142
|
+
lost_note TEXT,
|
|
143
|
+
note TEXT,
|
|
144
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
145
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
146
|
+
);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_deals_stage ON deals(stage, expected_close_date);
|
|
148
|
+
CREATE INDEX IF NOT EXISTS idx_deals_customer ON deals(customer_id);
|
|
149
|
+
|
|
150
|
+
CREATE TABLE IF NOT EXISTS deal_stage_log (
|
|
151
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
152
|
+
deal_id TEXT NOT NULL REFERENCES deals(id) ON DELETE CASCADE,
|
|
153
|
+
from_stage TEXT,
|
|
154
|
+
to_stage TEXT NOT NULL,
|
|
155
|
+
note TEXT,
|
|
156
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
157
|
+
);
|
|
158
|
+
CREATE INDEX IF NOT EXISTS idx_stage_log_deal ON deal_stage_log(deal_id, created_at);
|
|
159
|
+
|
|
160
|
+
CREATE TABLE IF NOT EXISTS monthly_targets (
|
|
161
|
+
month TEXT PRIMARY KEY,
|
|
162
|
+
target_premium REAL NOT NULL,
|
|
163
|
+
note TEXT,
|
|
164
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
165
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
CREATE TABLE IF NOT EXISTS sent_reminders (
|
|
169
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
170
|
+
reminder_type TEXT NOT NULL,
|
|
171
|
+
target_id TEXT NOT NULL,
|
|
172
|
+
bucket TEXT NOT NULL,
|
|
173
|
+
sent_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
|
174
|
+
status TEXT NOT NULL DEFAULT 'sent',
|
|
175
|
+
note TEXT,
|
|
176
|
+
UNIQUE (reminder_type, target_id, bucket)
|
|
177
|
+
);
|
|
178
|
+
CREATE INDEX IF NOT EXISTS idx_sent_target ON sent_reminders(target_id, reminder_type);
|
|
179
|
+
"""
|
|
180
|
+
)
|
|
181
|
+
conn.commit()
|
package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/insurance_paths.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""保险 4-Skill 统一路径解析(对齐美容院 skill 设计思路)。
|
|
2
|
+
|
|
3
|
+
参考 ``beauty-salon-member-appointment`` 的 ``orchestrator.py`` / ``product_service_cli``:
|
|
4
|
+
|
|
5
|
+
- ``skills_root()``:默认由包内文件向上回溯;可通过 ``INSURANCE_SKILLS_ROOT`` / ``SKILLS_ROOT``
|
|
6
|
+
覆盖(云端挂载目录与仓库不一致时)。
|
|
7
|
+
- 兄弟 skill:``<skills_root>/<skill>/src/src/<pkg>`` 与 ``/app/skills/...`` 多候选探测。
|
|
8
|
+
- 显式覆盖:与各包相关的 ``INSURANCE_*_PKG``;与各 skill **入口脚本** 相关的
|
|
9
|
+
``INSURANCE_*_CLI_SCRIPT``(对齐 ``INVENTORY_SCRIPT_PATH``:直接指向 ``cli.py``)。
|
|
10
|
+
- 子进程:若找到 ``scripts/cli.py``,优先 ``python cli.py ...``(子进程无需 PYTHONPATH);
|
|
11
|
+
否则回退 ``PYTHONPATH`` + ``python -m <module>``。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# skills 根目录
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
ENV_SKILLS_ROOT = ("INSURANCE_SKILLS_ROOT", "SKILLS_ROOT")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_skills_root(*, anchor_file: Path, parents_up_from_file: int) -> Path:
|
|
29
|
+
"""由任意锚点文件回溯 ``parents_up_from_file`` 层得到 ``skills/``;支持环境变量覆盖。"""
|
|
30
|
+
for key in ENV_SKILLS_ROOT:
|
|
31
|
+
raw = os.environ.get(key)
|
|
32
|
+
if raw:
|
|
33
|
+
return Path(raw).expanduser().resolve()
|
|
34
|
+
p = anchor_file.resolve()
|
|
35
|
+
for _ in range(parents_up_from_file):
|
|
36
|
+
p = p.parent
|
|
37
|
+
return p
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def skills_root_from_orchestrator(orchestrator_py: Path) -> Path:
|
|
41
|
+
"""``.../skills/<skill>/src/src/<pkg>/orchestrator.py`` → ``skills``(向上 5 层)。"""
|
|
42
|
+
return resolve_skills_root(anchor_file=orchestrator_py, parents_up_from_file=5)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def skills_root_from_dashboard_script(dashboard_py: Path) -> Path:
|
|
46
|
+
"""``.../skills/<skill>/src/scripts/dashboard_all.py`` → ``skills``(向上 4 层)。"""
|
|
47
|
+
return resolve_skills_root(anchor_file=dashboard_py, parents_up_from_file=4)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def skills_root_from_wrapper(wrapper_py: Path) -> Path:
|
|
51
|
+
"""``.../skills/<skill>/src/scripts/cli.py`` → ``skills``(向上 4 层)。"""
|
|
52
|
+
return resolve_skills_root(anchor_file=wrapper_py, parents_up_from_file=4)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# 各 skill 元数据(文件夹名、python 模块名、环境变量)
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
SKILL_META: dict[str, dict[str, str]] = {
|
|
60
|
+
"insurance-customer-policy": {
|
|
61
|
+
"module": "insurance_customer_cli",
|
|
62
|
+
"pkg_env": "INSURANCE_CUSTOMER_PKG",
|
|
63
|
+
"cli_env": "INSURANCE_CUSTOMER_CLI_SCRIPT",
|
|
64
|
+
},
|
|
65
|
+
"insurance-product-analysis": {
|
|
66
|
+
"module": "insurance_product_cli",
|
|
67
|
+
"pkg_env": "INSURANCE_PRODUCT_PKG",
|
|
68
|
+
"cli_env": "INSURANCE_PRODUCT_CLI_SCRIPT",
|
|
69
|
+
},
|
|
70
|
+
"insurance-schedule-renewal": {
|
|
71
|
+
"module": "insurance_schedule_cli",
|
|
72
|
+
"pkg_env": "INSURANCE_SCHEDULE_PKG",
|
|
73
|
+
"cli_env": "INSURANCE_SCHEDULE_CLI_SCRIPT",
|
|
74
|
+
},
|
|
75
|
+
"insurance-sales-pipeline": {
|
|
76
|
+
"module": "insurance_pipeline_cli",
|
|
77
|
+
"pkg_env": "INSURANCE_PIPELINE_PKG",
|
|
78
|
+
"cli_env": "INSURANCE_PIPELINE_CLI_SCRIPT",
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_pkg_parent(skills_root: Path, skill_folder: str) -> Path:
|
|
84
|
+
"""返回应加入 PYTHONPATH 的目录(含 ``<module>/__init__.py``)。"""
|
|
85
|
+
meta = SKILL_META[skill_folder]
|
|
86
|
+
pkg_name = meta["module"]
|
|
87
|
+
env_pkg = meta["pkg_env"]
|
|
88
|
+
|
|
89
|
+
if os.environ.get(env_pkg):
|
|
90
|
+
return Path(os.environ[env_pkg]).expanduser().resolve()
|
|
91
|
+
|
|
92
|
+
root = skills_root.resolve()
|
|
93
|
+
candidates = [
|
|
94
|
+
root / skill_folder / "src" / "src",
|
|
95
|
+
Path("/app/skills") / skill_folder / "src" / "src",
|
|
96
|
+
Path("/app/skills") / skill_folder / "src",
|
|
97
|
+
Path("/app/skills") / skill_folder,
|
|
98
|
+
root / skill_folder / "src",
|
|
99
|
+
root / skill_folder,
|
|
100
|
+
]
|
|
101
|
+
for c in candidates:
|
|
102
|
+
try:
|
|
103
|
+
if (c / pkg_name / "__init__.py").is_file():
|
|
104
|
+
return c
|
|
105
|
+
except OSError:
|
|
106
|
+
continue
|
|
107
|
+
return candidates[0]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def resolve_cli_wrapper(skills_root: Path, skill_folder: str) -> Path | None:
|
|
111
|
+
"""解析 ``scripts/cli.py``;支持 ``INSURANCE_*_CLI_SCRIPT`` 显式路径。"""
|
|
112
|
+
meta = SKILL_META[skill_folder]
|
|
113
|
+
env_cli = meta["cli_env"]
|
|
114
|
+
if os.environ.get(env_cli):
|
|
115
|
+
p = Path(os.environ[env_cli]).expanduser().resolve()
|
|
116
|
+
return p if p.is_file() else None
|
|
117
|
+
|
|
118
|
+
root = skills_root.resolve()
|
|
119
|
+
candidates = [
|
|
120
|
+
root / skill_folder / "src" / "scripts" / "cli.py",
|
|
121
|
+
root / skill_folder / "scripts" / "cli.py",
|
|
122
|
+
Path("/app/skills") / skill_folder / "src" / "scripts" / "cli.py",
|
|
123
|
+
Path("/app/skills") / skill_folder / "scripts" / "cli.py",
|
|
124
|
+
]
|
|
125
|
+
for c in candidates:
|
|
126
|
+
try:
|
|
127
|
+
if c.is_file():
|
|
128
|
+
return c
|
|
129
|
+
except OSError:
|
|
130
|
+
continue
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def run_skill_argv_env(
|
|
135
|
+
skills_root: Path,
|
|
136
|
+
skill_folder: str,
|
|
137
|
+
argv: list[str],
|
|
138
|
+
*,
|
|
139
|
+
timeout: int = 120,
|
|
140
|
+
) -> subprocess.CompletedProcess[str]:
|
|
141
|
+
"""运行某一 insurance skill 子命令;优先 wrapper,其次 ``python -m``。"""
|
|
142
|
+
meta = SKILL_META[skill_folder]
|
|
143
|
+
module = meta["module"]
|
|
144
|
+
cli = resolve_cli_wrapper(skills_root, skill_folder)
|
|
145
|
+
env = os.environ.copy()
|
|
146
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
147
|
+
|
|
148
|
+
if cli is not None:
|
|
149
|
+
return subprocess.run(
|
|
150
|
+
[sys.executable, str(cli), *argv],
|
|
151
|
+
env=env,
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
encoding="utf-8",
|
|
155
|
+
timeout=timeout,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
pkg = resolve_pkg_parent(skills_root, skill_folder)
|
|
159
|
+
if not (pkg / module / "__init__.py").is_file():
|
|
160
|
+
return subprocess.CompletedProcess(
|
|
161
|
+
args=[sys.executable, "-m", module, *argv],
|
|
162
|
+
returncode=99,
|
|
163
|
+
stdout="",
|
|
164
|
+
stderr=f"missing package at {pkg}",
|
|
165
|
+
)
|
|
166
|
+
env["PYTHONPATH"] = str(pkg)
|
|
167
|
+
return subprocess.run(
|
|
168
|
+
[sys.executable, "-m", module, *argv],
|
|
169
|
+
env=env,
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
encoding="utf-8",
|
|
173
|
+
timeout=timeout,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def bootstrap_wrapper_sys_path(wrapper_py: Path, skill_folder: str) -> Path:
|
|
178
|
+
"""供各 skill 的 ``scripts/cli.py`` 调用:把正确 pkg 目录插入 ``sys.path`` 首部。"""
|
|
179
|
+
sr = skills_root_from_wrapper(wrapper_py)
|
|
180
|
+
pkg_parent = resolve_pkg_parent(sr, skill_folder)
|
|
181
|
+
ps = str(pkg_parent)
|
|
182
|
+
if ps not in sys.path:
|
|
183
|
+
sys.path.insert(0, ps)
|
|
184
|
+
return pkg_parent
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""End-to-end smoke for insurance-customer-policy.
|
|
3
|
+
|
|
4
|
+
Runs every CLI subcommand against an isolated SQLite (set via
|
|
5
|
+
``INSURANCE_CUSTOMER_DB_PATH``) and asserts the responses are well-formed.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
HERE = Path(__file__).resolve().parent
|
|
17
|
+
PKG = HERE
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run(*args: str) -> dict:
|
|
21
|
+
cmd = [sys.executable, "-m", "insurance_customer_cli", *args]
|
|
22
|
+
env = {**os.environ, "PYTHONPATH": str(PKG), "PYTHONIOENCODING": "utf-8"}
|
|
23
|
+
r = subprocess.run(cmd, env=env, capture_output=True, text=True,
|
|
24
|
+
encoding="utf-8", timeout=60)
|
|
25
|
+
if r.returncode not in (0, 1):
|
|
26
|
+
raise SystemExit(f"unexpected rc={r.returncode}\nstdout={r.stdout}\nstderr={r.stderr}")
|
|
27
|
+
if not r.stdout.strip():
|
|
28
|
+
raise SystemExit(f"empty stdout for {args}\nstderr={r.stderr}")
|
|
29
|
+
return json.loads(r.stdout.strip())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def assert_ok(data: dict, *args: str) -> dict:
|
|
33
|
+
if not data.get("ok"):
|
|
34
|
+
raise SystemExit(f"expected ok=True, got {data} for args={args}")
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def assert_err(data: dict, code: str, *args: str) -> None:
|
|
39
|
+
if data.get("ok") or data.get("error") != code:
|
|
40
|
+
raise SystemExit(f"expected error={code}, got {data} for args={args}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main() -> int:
|
|
44
|
+
tmp = tempfile.mkdtemp(prefix="ins_cust_smoke_")
|
|
45
|
+
os.environ["INSURANCE_CUSTOMER_DB_PATH"] = str(Path(tmp) / "customer.sqlite3")
|
|
46
|
+
|
|
47
|
+
print("[1] customer-add")
|
|
48
|
+
d = run("customer-add", "--name", "张先生", "--phone", "13800138000",
|
|
49
|
+
"--age", "35", "--income", "300000",
|
|
50
|
+
"--household", '{"spouse":true,"children":2}', "--birthday", "1989-05-09")
|
|
51
|
+
cid = assert_ok(d)["id"]
|
|
52
|
+
assert cid == "C001", f"expected C001 got {cid}"
|
|
53
|
+
|
|
54
|
+
d = run("customer-add", "--name", "李女士", "--phone", "13900139000", "--age", "40")
|
|
55
|
+
cid2 = assert_ok(d)["id"]
|
|
56
|
+
assert cid2 == "C002"
|
|
57
|
+
|
|
58
|
+
print("[2] customer-query (keyword)")
|
|
59
|
+
d = assert_ok(run("customer-query", "--keyword", "张"))
|
|
60
|
+
assert d["count"] == 1 and d["customers"][0]["id"] == "C001"
|
|
61
|
+
|
|
62
|
+
print("[3] customer-query (id, with policy_count/last_followup_at)")
|
|
63
|
+
d = assert_ok(run("customer-query", "--customer-id", "C001"))
|
|
64
|
+
assert d["customers"][0]["policy_count"] == 0
|
|
65
|
+
assert d["customers"][0]["last_followup_at"] is None
|
|
66
|
+
|
|
67
|
+
print("[4] customer-update (set income + children)")
|
|
68
|
+
assert_ok(run("customer-update", "--customer-id", "C001",
|
|
69
|
+
"--set", '{"income":350000,"children":3,"tags":["VIP"]}'))
|
|
70
|
+
d = assert_ok(run("customer-query", "--customer-id", "C001"))
|
|
71
|
+
cust = d["customers"][0]
|
|
72
|
+
assert cust["income"] == 350000
|
|
73
|
+
assert cust["household"].get("children") == 3
|
|
74
|
+
assert cust["tags"] == ["VIP"]
|
|
75
|
+
|
|
76
|
+
print("[5] customer-exists")
|
|
77
|
+
assert assert_ok(run("customer-exists", "--customer-id", "C001"))["exists"] is True
|
|
78
|
+
assert assert_ok(run("customer-exists", "--customer-id", "C999"))["exists"] is False
|
|
79
|
+
|
|
80
|
+
print("[6] policy-add (with pay-period derivation)")
|
|
81
|
+
d = assert_ok(run("policy-add", "--customer-id", "C001",
|
|
82
|
+
"--company", "A保险公司", "--product", "重疾险XX",
|
|
83
|
+
"--type", "重疾险", "--coverage", "500000", "--premium", "8000",
|
|
84
|
+
"--effective-date", "2023-01-01", "--pay-period", "20年"))
|
|
85
|
+
pid = d["id"]
|
|
86
|
+
assert pid == "P001"
|
|
87
|
+
assert d["expire_date"] == "2043-01-01", d
|
|
88
|
+
assert d["next_pay_date"] == "2024-01-01", d
|
|
89
|
+
|
|
90
|
+
assert_ok(run("policy-add", "--customer-id", "C001",
|
|
91
|
+
"--company", "B保险公司", "--product", "百万医疗",
|
|
92
|
+
"--type", "医疗险", "--coverage", "1000000", "--premium", "500",
|
|
93
|
+
"--effective-date", "2023-06-01", "--pay-period", "1年"))
|
|
94
|
+
|
|
95
|
+
print("[7] policy-add: customer not found")
|
|
96
|
+
assert_err(run("policy-add", "--customer-id", "C999",
|
|
97
|
+
"--company", "X", "--product", "X", "--type", "重疾险",
|
|
98
|
+
"--coverage", "1", "--premium", "1",
|
|
99
|
+
"--effective-date", "2024-01-01"), "customer_not_found")
|
|
100
|
+
|
|
101
|
+
print("[8] policy-query")
|
|
102
|
+
d = assert_ok(run("policy-query", "--customer-id", "C001"))
|
|
103
|
+
assert d["count"] == 2
|
|
104
|
+
|
|
105
|
+
print("[9] policy-update + delete")
|
|
106
|
+
assert_ok(run("policy-update", "--policy-id", "P001",
|
|
107
|
+
"--set", '{"coverage":600000,"notes":"加保后"}'))
|
|
108
|
+
d = assert_ok(run("policy-query", "--policy-id", "P001"))
|
|
109
|
+
assert d["policies"][0]["coverage"] == 600000
|
|
110
|
+
|
|
111
|
+
print("[10] policy-count-by-customer")
|
|
112
|
+
assert assert_ok(run("policy-count-by-customer", "--customer-id", "C001"))["count"] == 2
|
|
113
|
+
|
|
114
|
+
print("[11] followup-add + query")
|
|
115
|
+
assert_ok(run("followup-add", "--customer-id", "C001",
|
|
116
|
+
"--content", "聊了养老险,很感兴趣",
|
|
117
|
+
"--next-step", "下周三见面详聊", "--next-date", "2024-05-15"))
|
|
118
|
+
assert_ok(run("followup-add", "--customer-id", "C002", "--content", "电话联系,下次再聊"))
|
|
119
|
+
d = assert_ok(run("followup-query", "--customer-id", "C001"))
|
|
120
|
+
assert d["count"] == 1
|
|
121
|
+
|
|
122
|
+
print("[12] gap-analysis (single)")
|
|
123
|
+
d = assert_ok(run("gap-analysis", "--customer-id", "C001"))
|
|
124
|
+
res = d["result"]
|
|
125
|
+
assert res["coverage"]["重疾险"] is True
|
|
126
|
+
assert res["coverage"]["医疗险"] is True
|
|
127
|
+
assert res["coverage"]["意外险"] is False
|
|
128
|
+
assert "意外险" in res["gaps"]
|
|
129
|
+
assert "教育金" in res["gaps"]
|
|
130
|
+
assert res["has_children"] is True
|
|
131
|
+
|
|
132
|
+
print("[13] gap-analysis (all + filter)")
|
|
133
|
+
d = assert_ok(run("gap-analysis", "--all", "--gap-type", "重疾险"))
|
|
134
|
+
cust_ids = [r["customer_id"] for r in d["results"]]
|
|
135
|
+
assert "C002" in cust_ids and "C001" not in cust_ids
|
|
136
|
+
|
|
137
|
+
print("[14] customer-segment")
|
|
138
|
+
d = assert_ok(run("customer-segment"))
|
|
139
|
+
assert d["summary"]["total"] == 2
|
|
140
|
+
assert any(c["customer_id"] == "C001" for c in d["active"])
|
|
141
|
+
|
|
142
|
+
print("[15] customer-delete (blocked by policies)")
|
|
143
|
+
assert_err(run("customer-delete", "--customer-id", "C001", "--yes"), "has_policies")
|
|
144
|
+
assert_ok(run("policy-delete", "--policy-id", "P001", "--yes"))
|
|
145
|
+
assert_ok(run("policy-delete", "--policy-id", "P002", "--yes"))
|
|
146
|
+
assert_err(run("customer-delete", "--customer-id", "C001"), "need_confirm")
|
|
147
|
+
assert_ok(run("customer-delete", "--customer-id", "C001", "--yes"))
|
|
148
|
+
|
|
149
|
+
print("[16] dashboard")
|
|
150
|
+
d = assert_ok(run("dashboard", "--json"))
|
|
151
|
+
assert d["customers_total"] == 1
|
|
152
|
+
assert d["policies_total"] == 0
|
|
153
|
+
|
|
154
|
+
print("[17] customer-field add/query")
|
|
155
|
+
assert_ok(run("customer-field-add", "--name", "风险偏好", "--type", "text"))
|
|
156
|
+
d = assert_ok(run("customer-field-query"))
|
|
157
|
+
assert any(f["name"] == "风险偏好" for f in d["fields"])
|
|
158
|
+
|
|
159
|
+
print("ALL OK")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
raise SystemExit(main())
|