sophhub 0.1.0

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 (125) hide show
  1. package/bin/sophhub.js +21 -0
  2. package/package.json +32 -0
  3. package/skills/VERSIONS.md +27 -0
  4. package/skills/builtin/clawhub/SKILL.md +77 -0
  5. package/skills/builtin/flight-booking/SKILL.md +288 -0
  6. package/skills/builtin/flight-booking/scripts/flight_booking.py +1232 -0
  7. package/skills/builtin/inventory-management/SKILL.md +241 -0
  8. package/skills/builtin/inventory-management/scripts/inventory.py +1844 -0
  9. package/skills/builtin/schedule-reminder/SKILL.md +619 -0
  10. package/skills/builtin/schedule-reminder/schedule_template.md +68 -0
  11. package/skills/builtin/schedule-reminder/scripts/append_event.py +204 -0
  12. package/skills/builtin/schedule-reminder/scripts/create_reminders.sh +163 -0
  13. package/skills/builtin/schedule-reminder/scripts/daily_activate.sh +175 -0
  14. package/skills/builtin/schedule-reminder/scripts/parse_schedule.py +704 -0
  15. package/skills/builtin/schedule-reminder/scripts/setup.sh +242 -0
  16. package/skills/builtin/schedule-reminder//347/224/250/346/210/267/346/214/207/345/215/227.md +311 -0
  17. package/skills/builtin/skill-creator/SKILL.md +370 -0
  18. package/skills/builtin/skill-creator/license.txt +202 -0
  19. package/skills/builtin/skill-creator/scripts/init_skill.py +378 -0
  20. package/skills/builtin/skill-creator/scripts/package_skill.py +111 -0
  21. package/skills/builtin/skill-creator/scripts/quick_validate.py +101 -0
  22. package/skills/builtin/sophnet-customer-management/SKILL.md +271 -0
  23. package/skills/builtin/sophnet-customer-management/pyproject.toml +15 -0
  24. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__init__.py +2 -0
  25. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__main__.py +5 -0
  26. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/cli.py +67 -0
  27. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/__init__.py +2 -0
  28. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/customer.py +60 -0
  29. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/export_file.py +18 -0
  30. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/import_file.py +15 -0
  31. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/reminder.py +26 -0
  32. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/schema.py +28 -0
  33. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/config.py +54 -0
  34. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/__init__.py +2 -0
  35. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/exporter.py +85 -0
  36. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/models.py +84 -0
  37. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/normalizer.py +144 -0
  38. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/parser.py +241 -0
  39. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/query.py +109 -0
  40. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/reminder.py +121 -0
  41. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/repository.py +397 -0
  42. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/schema.py +106 -0
  43. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/service.py +565 -0
  44. package/skills/builtin/sophnet-customer-management/uv.lock +48 -0
  45. package/skills/builtin/sophnet-customized-marketing/SKILL.md +144 -0
  46. package/skills/builtin/sophnet-customized-marketing/playbooks/campaign-planning.md +187 -0
  47. package/skills/builtin/sophnet-customized-marketing/playbooks/content-generation.md +124 -0
  48. package/skills/builtin/sophnet-customized-marketing/playbooks/marketing-calendar.md +59 -0
  49. package/skills/builtin/sophnet-customized-marketing/playbooks/multi-channel-bundle.md +94 -0
  50. package/skills/builtin/sophnet-customized-marketing/playbooks/poster-generation.md +182 -0
  51. package/skills/builtin/sophnet-customized-marketing/playbooks/style-profile-workflow.md +103 -0
  52. package/skills/builtin/sophnet-customized-marketing/pyproject.toml +9 -0
  53. package/skills/builtin/sophnet-customized-marketing/references/campaign-mechanics.md +168 -0
  54. package/skills/builtin/sophnet-customized-marketing/references/content-safety.md +26 -0
  55. package/skills/builtin/sophnet-customized-marketing/references/marketing-date-checklist.md +99 -0
  56. package/skills/builtin/sophnet-customized-marketing/references/platform-writing-guidelines.md +88 -0
  57. package/skills/builtin/sophnet-customized-marketing/references/quality-checklist.md +44 -0
  58. package/skills/builtin/sophnet-customized-marketing/scripts/generate_poster.py +585 -0
  59. package/skills/builtin/sophnet-customized-marketing/scripts/style_profile.py +215 -0
  60. package/skills/builtin/sophnet-face-search/SKILL.md +115 -0
  61. package/skills/builtin/sophnet-face-search/pyproject.toml +11 -0
  62. package/skills/builtin/sophnet-face-search/scripts/face_search.py +336 -0
  63. package/skills/builtin/sophnet-face-search/uv.lock +508 -0
  64. package/skills/builtin/sophnet-image-edit/SKILL.md +140 -0
  65. package/skills/builtin/sophnet-image-edit/pyproject.toml +9 -0
  66. package/skills/builtin/sophnet-image-edit/scripts/edit_and_preview.sh +68 -0
  67. package/skills/builtin/sophnet-image-edit/scripts/edit_image.py +279 -0
  68. package/skills/builtin/sophnet-image-edit/uv.lock +234 -0
  69. package/skills/builtin/sophnet-image-generate/SKILL.md +62 -0
  70. package/skills/builtin/sophnet-image-generate/pyproject.toml +9 -0
  71. package/skills/builtin/sophnet-image-generate/scripts/generate_image.py +156 -0
  72. package/skills/builtin/sophnet-image-generate/uv.lock +234 -0
  73. package/skills/builtin/sophnet-image-ocr/SKILL.md +167 -0
  74. package/skills/builtin/sophnet-image-ocr/pyproject.toml +13 -0
  75. package/skills/builtin/sophnet-image-ocr/scripts/ocr.py +226 -0
  76. package/skills/builtin/sophnet-image-ocr/uv.lock +234 -0
  77. package/skills/builtin/sophnet-infinite-talk/SKILL.md +140 -0
  78. package/skills/builtin/sophnet-infinite-talk/pyproject.toml +9 -0
  79. package/skills/builtin/sophnet-infinite-talk/scripts/gen.py +172 -0
  80. package/skills/builtin/sophnet-oss/SKILL.md +109 -0
  81. package/skills/builtin/sophnet-oss/pyproject.toml +8 -0
  82. package/skills/builtin/sophnet-oss/scripts/upload_file.py +43 -0
  83. package/skills/builtin/sophnet-qa-install/SKILL.md +210 -0
  84. package/skills/builtin/sophnet-qa-install/pyproject.toml +6 -0
  85. package/skills/builtin/sophnet-qa-install/scripts/backup_md.py +35 -0
  86. package/skills/builtin/sophnet-qa-install/scripts/check_installed.py +143 -0
  87. package/skills/builtin/sophnet-qa-install/scripts/update_config.py +142 -0
  88. package/skills/builtin/sophnet-qa-install/scripts/update_md.py +73 -0
  89. package/skills/builtin/sophnet-training-install/SKILL.md +211 -0
  90. package/skills/builtin/sophnet-training-install/pyproject.toml +6 -0
  91. package/skills/builtin/sophnet-training-install/scripts/backup_md.py +35 -0
  92. package/skills/builtin/sophnet-training-install/scripts/check_installed.py +144 -0
  93. package/skills/builtin/sophnet-training-install/scripts/update_config.py +142 -0
  94. package/skills/builtin/sophnet-training-install/scripts/update_md.py +73 -0
  95. package/skills/builtin/sophnet-tts/SKILL.md +79 -0
  96. package/skills/builtin/sophnet-tts/pyproject.toml +9 -0
  97. package/skills/builtin/sophnet-tts/scripts/gen_tts.py +130 -0
  98. package/skills/builtin/sophnet-video-generate/SKILL.md +116 -0
  99. package/skills/builtin/sophnet-video-generate/scripts/gen_video.py +304 -0
  100. package/skills/builtin/video-understand/SKILL.md +79 -0
  101. package/skills/builtin/video-understand/scripts/video_understand.py +204 -0
  102. package/skills/builtin/weather/SKILL.md +112 -0
  103. package/skills/builtin/web-scraper/SKILL.md +101 -0
  104. package/skills/builtin/web-scraper/scripts/scrape.py +270 -0
  105. package/skills/builtin/website-builder/SKILL.md +266 -0
  106. package/skills/builtin/website-builder/scripts/deploy_site.sh +46 -0
  107. package/skills/store/didi-ride/SKILL.md +309 -0
  108. package/skills/store/didi-ride/_meta.json +6 -0
  109. package/skills/store/didi-ride/assets/PREFERENCE.md +58 -0
  110. package/skills/store/didi-ride/package.json +15 -0
  111. package/skills/store/didi-ride/references/api_references.md +171 -0
  112. package/skills/store/didi-ride/references/error_handling.md +68 -0
  113. package/skills/store/didi-ride/references/setup.md +73 -0
  114. package/skills/store/didi-ride/references/workflow.md +150 -0
  115. package/skills/store/flyai/SKILL.md +119 -0
  116. package/skills/store/flyai/references/fliggy-fast-search.md +53 -0
  117. package/skills/store/flyai/references/search-flight.md +89 -0
  118. package/skills/store/flyai/references/search-hotels.md +57 -0
  119. package/skills/store/flyai/references/search-poi.md +49 -0
  120. package/src/commands/download.js +103 -0
  121. package/src/commands/list.js +67 -0
  122. package/src/utils/config.js +24 -0
  123. package/src/utils/gitlab.js +67 -0
  124. package/src/utils/paths.js +19 -0
  125. package/src/utils/versions.js +38 -0
@@ -0,0 +1,397 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ import uuid
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+ from typing import Any, Iterator
9
+
10
+
11
+ SCHEMA_SQL = """
12
+ CREATE TABLE IF NOT EXISTS customers (
13
+ customer_id TEXT PRIMARY KEY,
14
+ name TEXT NOT NULL,
15
+ primary_phone TEXT,
16
+ birthday TEXT,
17
+ tags_json TEXT NOT NULL DEFAULT '[]',
18
+ notes TEXT,
19
+ created_at TEXT NOT NULL,
20
+ updated_at TEXT NOT NULL
21
+ );
22
+
23
+ CREATE TABLE IF NOT EXISTS customer_field_defs (
24
+ field_key TEXT PRIMARY KEY,
25
+ label TEXT NOT NULL,
26
+ value_type TEXT NOT NULL,
27
+ entity_type TEXT NOT NULL DEFAULT 'customer',
28
+ is_required INTEGER NOT NULL DEFAULT 0,
29
+ source TEXT NOT NULL DEFAULT 'user_confirmed',
30
+ status TEXT NOT NULL DEFAULT 'active',
31
+ aliases_json TEXT NOT NULL DEFAULT '[]',
32
+ created_at TEXT NOT NULL,
33
+ updated_at TEXT NOT NULL
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS customer_field_values (
37
+ entity_type TEXT NOT NULL DEFAULT 'customer',
38
+ entity_id TEXT NOT NULL,
39
+ field_key TEXT NOT NULL,
40
+ value TEXT,
41
+ normalized_value TEXT,
42
+ display_value TEXT,
43
+ updated_at TEXT NOT NULL,
44
+ PRIMARY KEY (entity_type, entity_id, field_key)
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS business_records (
48
+ record_id TEXT PRIMARY KEY,
49
+ customer_id TEXT NOT NULL,
50
+ record_type TEXT NOT NULL,
51
+ status TEXT,
52
+ record_date TEXT,
53
+ amount TEXT,
54
+ title TEXT,
55
+ raw_payload_json TEXT NOT NULL DEFAULT '{}',
56
+ created_at TEXT NOT NULL,
57
+ updated_at TEXT NOT NULL
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS reminders (
61
+ reminder_id TEXT PRIMARY KEY,
62
+ customer_id TEXT NOT NULL,
63
+ reminder_type TEXT NOT NULL,
64
+ title TEXT NOT NULL,
65
+ trigger_at TEXT,
66
+ cron_expr TEXT NOT NULL,
67
+ message TEXT NOT NULL,
68
+ recurring INTEGER NOT NULL DEFAULT 0,
69
+ scheduled INTEGER NOT NULL DEFAULT 0,
70
+ external_task_name TEXT,
71
+ created_at TEXT NOT NULL
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS import_jobs (
75
+ import_job_id TEXT PRIMARY KEY,
76
+ source_file TEXT NOT NULL,
77
+ mode TEXT NOT NULL,
78
+ summary_json TEXT NOT NULL,
79
+ created_at TEXT NOT NULL
80
+ );
81
+
82
+ CREATE INDEX IF NOT EXISTS idx_customers_name ON customers(name);
83
+ CREATE INDEX IF NOT EXISTS idx_customers_phone ON customers(primary_phone);
84
+ CREATE INDEX IF NOT EXISTS idx_field_values_lookup ON customer_field_values(field_key, normalized_value);
85
+ CREATE INDEX IF NOT EXISTS idx_records_customer ON business_records(customer_id);
86
+ CREATE INDEX IF NOT EXISTS idx_reminders_customer ON reminders(customer_id);
87
+ """
88
+
89
+
90
+ class SQLiteRepository:
91
+ def __init__(self, db_path: str | Path):
92
+ self.db_path = Path(db_path)
93
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
94
+
95
+ @contextmanager
96
+ def connect(self) -> Iterator[sqlite3.Connection]:
97
+ conn = sqlite3.connect(self.db_path)
98
+ conn.row_factory = sqlite3.Row
99
+ try:
100
+ yield conn
101
+ conn.commit()
102
+ finally:
103
+ conn.close()
104
+
105
+ def init_db(self) -> None:
106
+ with self.connect() as conn:
107
+ conn.executescript(SCHEMA_SQL)
108
+
109
+ def save_import_job(self, source_file: str, mode: str, summary: dict[str, Any], created_at: str) -> str:
110
+ job_id = f"imp_{uuid.uuid4().hex[:12]}"
111
+ with self.connect() as conn:
112
+ conn.execute(
113
+ """
114
+ INSERT INTO import_jobs (import_job_id, source_file, mode, summary_json, created_at)
115
+ VALUES (?, ?, ?, ?, ?)
116
+ """,
117
+ (job_id, source_file, mode, json.dumps(summary, ensure_ascii=False), created_at),
118
+ )
119
+ return job_id
120
+
121
+ def list_field_defs(self, entity_type: str = "customer") -> list[dict[str, Any]]:
122
+ with self.connect() as conn:
123
+ rows = conn.execute(
124
+ """
125
+ SELECT * FROM customer_field_defs
126
+ WHERE entity_type = ?
127
+ ORDER BY label COLLATE NOCASE
128
+ """,
129
+ (entity_type,),
130
+ ).fetchall()
131
+ results = []
132
+ for row in rows:
133
+ item = dict(row)
134
+ item["aliases"] = json.loads(item.pop("aliases_json"))
135
+ results.append(item)
136
+ return results
137
+
138
+ def upsert_field_def(self, field_def: dict[str, Any], now: str) -> None:
139
+ aliases_json = json.dumps(field_def.get("aliases", []), ensure_ascii=False)
140
+ with self.connect() as conn:
141
+ conn.execute(
142
+ """
143
+ INSERT INTO customer_field_defs
144
+ (field_key, label, value_type, entity_type, is_required, source, status, aliases_json, created_at, updated_at)
145
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
146
+ ON CONFLICT(field_key) DO UPDATE SET
147
+ label = excluded.label,
148
+ value_type = excluded.value_type,
149
+ entity_type = excluded.entity_type,
150
+ is_required = excluded.is_required,
151
+ source = excluded.source,
152
+ status = excluded.status,
153
+ aliases_json = excluded.aliases_json,
154
+ updated_at = excluded.updated_at
155
+ """,
156
+ (
157
+ field_def["field_key"],
158
+ field_def["label"],
159
+ field_def["value_type"],
160
+ field_def.get("entity_type", "customer"),
161
+ int(bool(field_def.get("is_required", False))),
162
+ field_def.get("source", "user_confirmed"),
163
+ field_def.get("status", "active"),
164
+ aliases_json,
165
+ now,
166
+ now,
167
+ ),
168
+ )
169
+
170
+ def upsert_customer(self, payload: dict[str, Any], now: str) -> tuple[str, bool]:
171
+ customer_id = payload.get("customer_id")
172
+ existing = self.find_customer_for_identity(
173
+ name=payload["name"],
174
+ primary_phone=payload.get("primary_phone"),
175
+ birthday=payload.get("birthday"),
176
+ )
177
+ created = False
178
+ if existing:
179
+ customer_id = existing["customer_id"]
180
+ if not customer_id:
181
+ customer_id = f"cus_{uuid.uuid4().hex[:12]}"
182
+ created = True
183
+
184
+ with self.connect() as conn:
185
+ conn.execute(
186
+ """
187
+ INSERT INTO customers
188
+ (customer_id, name, primary_phone, birthday, tags_json, notes, created_at, updated_at)
189
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
190
+ ON CONFLICT(customer_id) DO UPDATE SET
191
+ name = excluded.name,
192
+ primary_phone = COALESCE(excluded.primary_phone, customers.primary_phone),
193
+ birthday = COALESCE(excluded.birthday, customers.birthday),
194
+ tags_json = excluded.tags_json,
195
+ notes = COALESCE(excluded.notes, customers.notes),
196
+ updated_at = excluded.updated_at
197
+ """,
198
+ (
199
+ customer_id,
200
+ payload["name"],
201
+ payload.get("primary_phone"),
202
+ payload.get("birthday"),
203
+ json.dumps(payload.get("tags", []), ensure_ascii=False),
204
+ payload.get("notes"),
205
+ now,
206
+ now,
207
+ ),
208
+ )
209
+ return customer_id, created
210
+
211
+ def find_customer_for_identity(
212
+ self,
213
+ *,
214
+ name: str,
215
+ primary_phone: str | None = None,
216
+ birthday: str | None = None,
217
+ ) -> dict[str, Any] | None:
218
+ with self.connect() as conn:
219
+ if primary_phone:
220
+ row = conn.execute(
221
+ """
222
+ SELECT * FROM customers
223
+ WHERE name = ? AND primary_phone = ?
224
+ LIMIT 1
225
+ """,
226
+ (name, primary_phone),
227
+ ).fetchone()
228
+ if row:
229
+ return self._customer_row_to_dict(row)
230
+ if birthday:
231
+ row = conn.execute(
232
+ """
233
+ SELECT * FROM customers
234
+ WHERE name = ? AND birthday = ?
235
+ LIMIT 1
236
+ """,
237
+ (name, birthday),
238
+ ).fetchone()
239
+ if row:
240
+ return self._customer_row_to_dict(row)
241
+ row = conn.execute(
242
+ """
243
+ SELECT * FROM customers
244
+ WHERE name = ?
245
+ LIMIT 1
246
+ """,
247
+ (name,),
248
+ ).fetchone()
249
+ return self._customer_row_to_dict(row) if row else None
250
+
251
+ def get_customer(self, customer_id: str) -> dict[str, Any] | None:
252
+ with self.connect() as conn:
253
+ row = conn.execute("SELECT * FROM customers WHERE customer_id = ?", (customer_id,)).fetchone()
254
+ return self._customer_row_to_dict(row) if row else None
255
+
256
+ def list_customers(self) -> list[dict[str, Any]]:
257
+ with self.connect() as conn:
258
+ rows = conn.execute(
259
+ "SELECT * FROM customers ORDER BY updated_at DESC, name COLLATE NOCASE"
260
+ ).fetchall()
261
+ return [self._customer_row_to_dict(row) for row in rows]
262
+
263
+ def delete_customer(self, customer_id: str) -> bool:
264
+ with self.connect() as conn:
265
+ deleted = conn.execute("DELETE FROM customers WHERE customer_id = ?", (customer_id,)).rowcount
266
+ conn.execute(
267
+ "DELETE FROM customer_field_values WHERE entity_type = 'customer' AND entity_id = ?",
268
+ (customer_id,),
269
+ )
270
+ conn.execute("DELETE FROM business_records WHERE customer_id = ?", (customer_id,))
271
+ conn.execute("DELETE FROM reminders WHERE customer_id = ?", (customer_id,))
272
+ return deleted > 0
273
+
274
+ def upsert_field_value(
275
+ self,
276
+ *,
277
+ entity_type: str,
278
+ entity_id: str,
279
+ field_key: str,
280
+ value: str | None,
281
+ normalized_value: str | None,
282
+ display_value: str | None,
283
+ now: str,
284
+ ) -> None:
285
+ with self.connect() as conn:
286
+ conn.execute(
287
+ """
288
+ INSERT INTO customer_field_values
289
+ (entity_type, entity_id, field_key, value, normalized_value, display_value, updated_at)
290
+ VALUES (?, ?, ?, ?, ?, ?, ?)
291
+ ON CONFLICT(entity_type, entity_id, field_key) DO UPDATE SET
292
+ value = excluded.value,
293
+ normalized_value = excluded.normalized_value,
294
+ display_value = excluded.display_value,
295
+ updated_at = excluded.updated_at
296
+ """,
297
+ (entity_type, entity_id, field_key, value, normalized_value, display_value, now),
298
+ )
299
+
300
+ def list_field_values(self, entity_type: str = "customer") -> list[dict[str, Any]]:
301
+ with self.connect() as conn:
302
+ rows = conn.execute(
303
+ """
304
+ SELECT * FROM customer_field_values
305
+ WHERE entity_type = ?
306
+ ORDER BY updated_at DESC
307
+ """,
308
+ (entity_type,),
309
+ ).fetchall()
310
+ return [dict(row) for row in rows]
311
+
312
+ def create_business_record(self, payload: dict[str, Any], now: str) -> str:
313
+ record_id = f"rec_{uuid.uuid4().hex[:12]}"
314
+ with self.connect() as conn:
315
+ conn.execute(
316
+ """
317
+ INSERT INTO business_records
318
+ (record_id, customer_id, record_type, status, record_date, amount, title, raw_payload_json, created_at, updated_at)
319
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
320
+ """,
321
+ (
322
+ record_id,
323
+ payload["customer_id"],
324
+ payload["record_type"],
325
+ payload.get("status"),
326
+ payload.get("record_date"),
327
+ payload.get("amount"),
328
+ payload.get("title"),
329
+ json.dumps(payload.get("raw_payload", {}), ensure_ascii=False),
330
+ now,
331
+ now,
332
+ ),
333
+ )
334
+ return record_id
335
+
336
+ def list_business_records(self, customer_id: str | None = None) -> list[dict[str, Any]]:
337
+ with self.connect() as conn:
338
+ if customer_id:
339
+ rows = conn.execute(
340
+ "SELECT * FROM business_records WHERE customer_id = ? ORDER BY updated_at DESC",
341
+ (customer_id,),
342
+ ).fetchall()
343
+ else:
344
+ rows = conn.execute(
345
+ "SELECT * FROM business_records ORDER BY updated_at DESC"
346
+ ).fetchall()
347
+ results = []
348
+ for row in rows:
349
+ item = dict(row)
350
+ item["raw_payload"] = json.loads(item.pop("raw_payload_json"))
351
+ results.append(item)
352
+ return results
353
+
354
+ def replace_reminders_for_customer(self, customer_id: str, reminder_type: str, reminders: list[dict[str, Any]], now: str) -> None:
355
+ with self.connect() as conn:
356
+ conn.execute(
357
+ "DELETE FROM reminders WHERE customer_id = ? AND reminder_type = ?",
358
+ (customer_id, reminder_type),
359
+ )
360
+ for reminder in reminders:
361
+ reminder_id = f"rmd_{uuid.uuid4().hex[:12]}"
362
+ conn.execute(
363
+ """
364
+ INSERT INTO reminders
365
+ (reminder_id, customer_id, reminder_type, title, trigger_at, cron_expr, message, recurring, scheduled, external_task_name, created_at)
366
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
367
+ """,
368
+ (
369
+ reminder_id,
370
+ customer_id,
371
+ reminder_type,
372
+ reminder["title"],
373
+ reminder.get("trigger_at"),
374
+ reminder["cron_expr"],
375
+ reminder["message"],
376
+ int(bool(reminder.get("recurring", False))),
377
+ int(bool(reminder.get("scheduled", False))),
378
+ reminder.get("external_task_name"),
379
+ now,
380
+ ),
381
+ )
382
+
383
+ def list_reminders(self, customer_id: str | None = None) -> list[dict[str, Any]]:
384
+ with self.connect() as conn:
385
+ if customer_id:
386
+ rows = conn.execute(
387
+ "SELECT * FROM reminders WHERE customer_id = ? ORDER BY created_at DESC",
388
+ (customer_id,),
389
+ ).fetchall()
390
+ else:
391
+ rows = conn.execute("SELECT * FROM reminders ORDER BY created_at DESC").fetchall()
392
+ return [dict(row) for row in rows]
393
+
394
+ def _customer_row_to_dict(self, row: sqlite3.Row) -> dict[str, Any]:
395
+ item = dict(row)
396
+ item["tags"] = json.loads(item.pop("tags_json"))
397
+ return item
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .models import BUILTIN_CUSTOMER_FIELDS, FieldDefinition
6
+ from .normalizer import clean_text, guess_value_type, stable_field_key
7
+
8
+
9
+ BUILTIN_FIELD_ALIASES: dict[str, set[str]] = {
10
+ "name": {"姓名", "名字", "客户名", "客户姓名", "name"},
11
+ "primary_phone": {"电话", "手机号", "手机", "联系电话", "联系方式", "mobile", "phone"},
12
+ "birthday": {"生日", "出生日期", "birth", "birthday"},
13
+ "tags": {"标签", "分组", "tags"},
14
+ "notes": {"备注", "说明", "note", "notes"},
15
+ }
16
+
17
+
18
+ GROUPED_RECORD_FIELD_ALIASES: dict[str, set[str]] = {
19
+ "record_no": {"记录号", "编号", "单号", "保单号", "保单编号", "订单号", "合同号", "record_no"},
20
+ "record_date": {"记录日期", "日期", "创建日期", "生效日", "生效日期", "保单生效日", "下单日"},
21
+ "relationship": {"关系", "客户关系", "投被保人关系"},
22
+ "item_name": {"项目名称", "产品名称", "商品名称", "服务名称", "险种", "产品"},
23
+ "amount": {"金额", "费用", "消费金额", "保费", "保费(元)", "amount", "price"},
24
+ "term": {"期限", "周期", "缴费期限", "缴费期"},
25
+ "owner_name": {"负责人", "业务员", "顾问", "原业务员", "销售"},
26
+ }
27
+
28
+
29
+ def build_alias_lookup(alias_map: dict[str, set[str]]) -> dict[str, str]:
30
+ lookup: dict[str, str] = {}
31
+ for field_key, aliases in alias_map.items():
32
+ for alias in aliases:
33
+ lookup[clean_text(alias).lower()] = field_key
34
+ return lookup
35
+
36
+
37
+ BUILTIN_ALIAS_LOOKUP = build_alias_lookup(BUILTIN_FIELD_ALIASES)
38
+ GROUPED_RECORD_ALIAS_LOOKUP = build_alias_lookup(GROUPED_RECORD_FIELD_ALIASES)
39
+
40
+
41
+ def resolve_builtin_field(label: str) -> str | None:
42
+ return BUILTIN_ALIAS_LOOKUP.get(clean_text(label).lower())
43
+
44
+
45
+ def resolve_grouped_record_field(label: str) -> str | None:
46
+ return GROUPED_RECORD_ALIAS_LOOKUP.get(clean_text(label).lower())
47
+
48
+
49
+ def resolve_existing_field(label: str, existing_defs: list[dict[str, Any]]) -> str | None:
50
+ normalized = clean_text(label).lower()
51
+ for item in existing_defs:
52
+ if normalized == clean_text(item["label"]).lower():
53
+ return item["field_key"]
54
+ aliases = item.get("aliases") or []
55
+ if normalized in {clean_text(alias).lower() for alias in aliases}:
56
+ return item["field_key"]
57
+ return None
58
+
59
+
60
+ def resolve_or_create_field(
61
+ *,
62
+ label: str,
63
+ sample_values: list[Any],
64
+ existing_defs: list[dict[str, Any]],
65
+ source: str,
66
+ ) -> FieldDefinition | None:
67
+ text = clean_text(label)
68
+ if not text:
69
+ return None
70
+ builtin_key = resolve_builtin_field(text)
71
+ if builtin_key:
72
+ return None
73
+ existing_key = resolve_existing_field(text, existing_defs)
74
+ field_key = existing_key or stable_field_key(text)
75
+ aliases = [text]
76
+ value_type = guess_value_type(text, sample_values)
77
+ return FieldDefinition(
78
+ field_key=field_key,
79
+ label=text,
80
+ value_type=value_type,
81
+ entity_type="customer",
82
+ is_required=False,
83
+ source=source,
84
+ status="active",
85
+ aliases=aliases,
86
+ )
87
+
88
+
89
+ def create_schema_from_labels(labels: list[str], existing_defs: list[dict[str, Any]]) -> list[FieldDefinition]:
90
+ results: list[FieldDefinition] = []
91
+ for label in labels:
92
+ if resolve_builtin_field(label):
93
+ continue
94
+ field_def = resolve_or_create_field(
95
+ label=label,
96
+ sample_values=[],
97
+ existing_defs=existing_defs,
98
+ source="nl_generated",
99
+ )
100
+ if field_def and field_def.field_key not in {item.field_key for item in results}:
101
+ results.append(field_def)
102
+ return results
103
+
104
+
105
+ def is_builtin_field(field_key: str) -> bool:
106
+ return field_key in BUILTIN_CUSTOMER_FIELDS