sdpy-kit 0.6.0__tar.gz → 0.6.2__tar.gz

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 (54) hide show
  1. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/PKG-INFO +81 -56
  2. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/README.md +80 -55
  3. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/__init__.py +1 -1
  4. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/audit.py +30 -1
  5. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/base_db_kit.py +13 -2
  6. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/.gitignore +0 -0
  7. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/__init__.py +0 -0
  8. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/base_service.py +0 -0
  9. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/chat_service.py +0 -0
  10. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/embedding_service.py +0 -0
  11. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/exceptions.py +0 -0
  12. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/failover.py +0 -0
  13. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/image_service.py +0 -0
  14. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/model_builder.py +0 -0
  15. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/model_client.py +0 -0
  16. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/pool.py +0 -0
  17. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/profiles.py +0 -0
  18. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/rerank_service.py +0 -0
  19. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/runner.py +0 -0
  20. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/ai/types.py +0 -0
  21. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/common_kit.py +0 -0
  22. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/config/__init__.py +0 -0
  23. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/config/kit_config.py +0 -0
  24. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/config/log_config.py +0 -0
  25. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/crawl/__init__.py +0 -0
  26. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/crawl/article_io.py +0 -0
  27. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/crawl/article_models.py +0 -0
  28. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/crawl/deepseek_kit.py +0 -0
  29. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/crawl/web_article_kit.py +0 -0
  30. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/crawl/wx_article_kit.py +0 -0
  31. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/__init__.py +0 -0
  32. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/crud.py +0 -0
  33. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/engine.py +0 -0
  34. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/exceptions.py +0 -0
  35. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/models/__init__.py +0 -0
  36. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/models/base.py +0 -0
  37. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/mysql_kit.py +0 -0
  38. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/pg_kit.py +0 -0
  39. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/query.py +0 -0
  40. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/session.py +0 -0
  41. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/sqlite_kit.py +0 -0
  42. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/db/transaction.py +0 -0
  43. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/docs/__init__.py +0 -0
  44. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/docs/anydoc_kit.py +0 -0
  45. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/docs/docx_kit.py +0 -0
  46. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/docs/marker_kit.py +0 -0
  47. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/docs/md2doc_kit.py +0 -0
  48. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/docs/mineru_kit.py +0 -0
  49. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/docs/rc_docparser_kit.py +0 -0
  50. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/file_kit.py +0 -0
  51. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/laya_kit.py +0 -0
  52. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/license_kit.py +0 -0
  53. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/kit/redis_kit.py +0 -0
  54. {sdpy_kit-0.6.0 → sdpy_kit-0.6.2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sdpy-kit
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/Redis/文件/文档/向量工具集
5
5
  Author-email: Clark <changhongyuan@126.com>
6
6
  License-Expression: MIT
@@ -80,7 +80,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
80
80
 
81
81
  - **统一数据库访问层**:PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致,切换数据库只改一行 import;内置审计字段自动填充(`created_*` / `updated_*` / `deleted`)与软删除查询过滤;
82
82
  - **AI 多模型服务池**:chat / embedding / rerank / image 四类能力,多厂商候选链 + 故障自动转移;
83
- - **工具集**:Redis / 文件 / 雪花 ID / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / 微信公众号文章抓取存储 / 通用网页文章抓取存储。
83
+ - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
84
84
 
85
85
  > 要求 Python 3.13+
86
86
 
@@ -94,6 +94,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
94
94
  | `kit.redis_kit` | `RedisKit` | Redis string / hash / list / set / zset | `redis` |
95
95
  | `kit.file_kit` | `FileKit` | 文件与目录读写、移动、遍历 | 无 |
96
96
  | `kit.common_kit` | `CommonKit` | 雪花 ID、时间戳 ID、时间格式化 | 无 |
97
+ | `kit.license_kit` | `LicenseKit` / `LicenseVerifier` | Ed25519 离线授权:密钥对、机器码、签发、验签(签名 → 产品 → 机器码 → 有效期) | 无 |
97
98
  | `kit.docs.anydoc_kit` | `AnyDocKit` | docx / pdf / xlsx 等 → Markdown | `anydoc` |
98
99
  | `kit.docs.docx_kit` | `DocxKit` | Word 模板动态填充(章节 / 列表 / 表格) | `docx` |
99
100
  | `kit.docs.rc_docparser_kit` | `RcDocParserKit` | 离线 docx → Markdown + Images(无模型) | `docx-parse` |
@@ -187,7 +188,7 @@ SQLITE_DB_PATH=./data/sqlite.db
187
188
 
188
189
  ### 环境变量 ↔ 能力对照表
189
190
 
190
- > 表中未出现的模块(`kit.crawl.article_models`、`kit.common_kit`、`kit.docs.docx_kit`、`kit.docs.md2doc_kit`、`kit.crawl.wx_article_kit`、`kit.crawl.web_article_kit`)不读取任何环境变量。
191
+ > 表中未出现的模块(`kit.crawl.article_models`、`kit.crawl.article_io`、`kit.common_kit`、`kit.docs.docx_kit`、`kit.docs.md2doc_kit`、`kit.license_kit`)不读取任何环境变量。
191
192
 
192
193
  **基础与日志**(影响 `kit.config` 及所有模块的日志输出)
193
194
 
@@ -272,6 +273,21 @@ SQLITE_DB_PATH=./data/sqlite.db
272
273
  > `LAYA_HF_*` 同样由 kit 在**导入 laya 之前**经 `os.environ.setdefault` 应用,不覆盖显式配置;
273
274
  > `LAYA_MODEL` / `LAYA_DEVICE` 由 `Settings` 读取(进程内缓存,改后需重启进程)。
274
275
 
276
+ **DeepSeek Search / Fetch** → `kit.crawl.deepseek_kit`
277
+
278
+ | 环境变量 | 默认值 | 作用 |
279
+ |----------|--------|------|
280
+ | `DEEPSEEK_API_KEY` | 无 | Search / Fetch 鉴权 Key(与模型对话共用同一套);缺失时调用抛 `DeepSeekKitError` |
281
+ | `DEEPSEEK_BASE_URL` | `https://api.deepseek.com` | API 基础地址 |
282
+ | `DEEPSEEK_TIMEOUT` | `60`(1–600) | 单次 HTTP 请求超时(秒) |
283
+ | `DEEPSEEK_MAX_RETRIES` | `2`(0–5) | 408 / 429 / 5xx 重试次数(不含首次请求);其它 4xx 不重试 |
284
+
285
+ **文章抓取长度自检** → `kit.crawl.wx_article_kit` / `web_article_kit` / `deepseek_kit`
286
+
287
+ | 环境变量 | 默认值 | 作用 |
288
+ |----------|--------|------|
289
+ | `CRAWL_MIN_CONTENT_CHARS` | `500`(≥0) | 正文短于该长度时写入 `warnings`(非致命,不改成功语义、不截断正文);`0` 关闭该判据 |
290
+
275
291
  **云 OCR**(任何 `ocr="hosted"` 的转换)→ `kit.docs.anydoc_kit`
276
292
 
277
293
  | 环境变量 | 默认值 | 作用 |
@@ -369,9 +385,9 @@ from kit import session_scope, transactional, DBError # 等价
369
385
  | 原生表 | `create_raw_table(table_name, columns, if_not_exists=True) -> None` | 按「列名 → 列定义」建普通表;⚠ 列定义须按目标库方言书写,不做方言翻译 |
370
386
  | 原生表 | `raw_insert(table_name, data, *, ignore_conflict=False) -> int` | 插入一行;`ignore_conflict=True` 幂等(PG/SQLite `ON CONFLICT DO NOTHING`、MySQL `INSERT IGNORE`) |
371
387
  | 原生表 | `raw_update(table_name, data, where, params=None) -> int` | 按 `where`(不含 `WHERE`)更新;启用审计填充时自动补 `updated_*` |
372
- | 原生表 | `raw_touch(table_name, where, params=None) -> int` | **0.6.0**:只刷新 `updated_*`(需先 `enable`;表无该列抛 `DBError`) |
373
- | 原生表 | `raw_delete(table_name, where, params=None) -> int` | **0.6.0**:条件删除(`where` 必填) |
374
- | 原生表 | `build_raw_insert / build_raw_update / build_raw_touch / build_raw_delete` | **0.6.0**:只拼装不执行,返回 `(语句, 参数)`,供**自有事务**内 `session.execute(*builder(...))` |
388
+ | 原生表 | `raw_touch(table_name, where, params=None) -> int` | 只刷新 `updated_*`(需先 `enable`;表无该列抛 `DBError`) |
389
+ | 原生表 | `raw_delete(table_name, where, params=None) -> int` | 条件删除(`where` 必填) |
390
+ | 原生表 | `build_raw_insert / build_raw_update / build_raw_touch / build_raw_delete` | 只拼装不执行,返回 `(语句, 参数)`,供**自有事务**内 `session.execute(*builder(...))` |
375
391
  | 原生表 | `raw_get_by(table_name, column, value) -> dict \| None` | 按单列等值取首行 |
376
392
  | 事务 | `transaction()` | 事务上下文管理器,`with PgKit.transaction() as session:` |
377
393
  | 表 | `list_tables() -> list[str]` | 列出全部表名 |
@@ -505,57 +521,9 @@ PgKit.insert_vectors("my_vec", [[0.1] * 384], ids=[1], id_col="vid", column="vec
505
521
  PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
506
522
  ```
507
523
 
508
- #### 0.2.2 行为变更(升级须知)
509
-
510
- | 编号 | 变更 | 影响 |
511
- |------|------|------|
512
- | B1 | `search_vectors` 新增 `exact: bool = True`,默认**精确** | SQLite 返回的 `distance` 由量化值(≈L2×255)变为**真值 L2**;需旧口径请显式 `exact=False` |
513
- | B2 | `filter_sql` 语义统一为**先过滤、再取 top_k** | 传 `filter_sql` 的调用返回值**变多/变全**(旧结果 ⊆ 新结果);曾自行放大 `top_k` 兜底的调用可收紧 |
514
- | B3 | 默认 `distance_type` 由 `"COSINE"` 改为 `"L2"` | 依赖默认值的调用方请显式传 `distance_type="COSINE"` |
515
- | B4 | `create_vector_table` 的 `dims` **必填**,参数顺序为 `(table_name, dims, id_col, column, extra_cols)` | `create_vector_table("t")` 类调用需补维度;位置传 `id_col` 的调用改为关键字 |
516
- | B5 | MySQL 的 `search_vectors` 异常由 `NotImplementedError` 改为 `VectorNotSupportedError`(继承 `DBError`) | `except NotImplementedError` 需改为 `except VectorNotSupportedError`(或 `except DBError`) |
517
-
518
- > 新增(非破坏性):`VECTOR_CAPABILITIES` 能力声明、`id_col` / `column` 逃生口参数、
519
- > `insert_vectors` 支持二维 numpy 数组与批量提交。
520
-
521
- #### 0.2.6 变更
522
-
523
- | 编号 | 变更 | 影响 |
524
- |------|------|------|
525
- | D1 | `prepare_statement` 的 `params` 支持**参数行列表**(`list[dict]`):**原样返回该列表** → `session.execute(*prepare_statement(sql, params))` 即批量执行(executemany),与 `build_insert_vectors` 的返回形态直接对接;行键集不一致或行值含序列 → 本地可操作 `ValueError`(与 `execute` 批量形态**同一规则**) | 向后兼容:单字典 / `None` 形态行为不变;行列表形态**不做** `IN` 展开(与批量互斥) |
526
- | D1 | 文档:修正「自有事务」示例——0.2.5 的示例把 `build_insert_vectors` 返回的**行列表**传给了单字典形态,照抄会抛 `AttributeError: 'list' object has no attribute 'items'` | **纯文档缺陷修正**(0.2.5 示例照抄即崩) |
527
- | — | 文档:批量执行补充 `rowcount` 备注——忽略型写入(如 SQLite `INSERT OR IGNORE`)的冲突行不计入 `rowcount`(即 `rowcount` = 实际插入数;⚠ 仅 SQLite 实测) | 文档 |
528
-
529
- #### 0.2.5 变更
530
-
531
- | 编号 | 变更 | 影响 |
532
- |------|------|------|
533
- | — | 新增**模块级公共函数** `prepare_statement(sql, params=None) -> (语句, 归一化参数)`:供调用方在**自有事务**内执行原生 SQL(含 `IN :ids`),**调用方无须 import `sqlalchemy`**;与 `query` / `execute` 同一实现(单一事实源) | **纯新增**:`query` / `transaction` 的签名与行为**零变更**;共享方法数仍为 **24**(模块级函数不计入) |
534
- | — | `execute` 的 `params` 支持**字典列表**(executemany,同一语句 + N 行参数一次往返);**空列表**不触库返回 `0` | 向后兼容:单字典 / `None` 形态行为不变;批量要求各行字典**键集一致**、行值为**标量**(与 `IN` 展开互斥),违规在本地抛可操作 `ValueError`(含行号),不触库 |
535
- | — | 文档:自有事务示例改用 `prepare_statement`(不再需要 `from sqlalchemy import text`);补「批量执行」说明与约束 | 文档 |
536
-
537
- > ⚠ `:name::type` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 `text()` 识别**(0.2.5 之前亦如此),
524
+ > ⚠ `:name::type` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 `text()` 识别**,
538
525
  > 请改用 `CAST(:name AS type)`。
539
526
 
540
- #### 0.2.4 变更
541
-
542
- | 编号 | 变更 | 影响 |
543
- |------|------|------|
544
- | B4 | **入参自洽性校验**(`extra_data` 的键):保留绑定名 `id` / `vec`、与 `id_col` / `column` 重复、不是合法标识符、`id_col == column` → 抛可操作的 `ValueError` | **唯一行为变更**:此前 SQLite 会**静默写坏数据**(如 `id` 列被覆盖)、PG/MySQL 报误导性错误;正常调用**零影响** |
545
- | — | 新增共享方法 `build_insert_vectors`(只拼装不执行,由 `BaseDbKit` 收敛 → 共享方法 23 → **24**) | 纯新增:可在**自有事务**内写向量行(见下) |
546
- | — | `create_vector_table` 的 `extra_cols` 支持 `{列名: 类型串}`(与 `create_raw_table` 同形) | 纯新增:既有 `[{"name","type"}]` 形态保留,两者产出 DDL 逐字一致 |
547
- | — | 后端差异收口:向量列占位符由 `VECTOR_PLACEHOLDER` 驱动(MySQL = `STRING_TO_VECTOR(:vec)`) | 内部整理:公开签名与行为不变 |
548
- | — | `SQLiteKit.insert_vectors` 的事务获取统一为 `transaction()`(与另两库同构) | 内部一致:行为等价(自成一事务并提交) |
549
-
550
- #### 0.2.3 变更
551
-
552
- | 编号 | 变更 | 影响 |
553
- |------|------|------|
554
- | B6 | **`PgKit.get_table_info` 按数据库解析语义定位表**(`search_path` 解析到的那张表) | 多 schema 下同名表**不再混行**;单 schema / `public` 场景**无影响**;只接受非限定表名 |
555
- | — | `query` / `execute` 支持**列表参数**(`IN :ids`,三库通用) | 纯新增;`set`/`frozenset` 自动归一化为 `list` |
556
- | — | 新增 `SQLiteKit.encode_vector` / `decode_vector`(小端 float32) | 纯新增:可在自有事务内写向量表,见下 |
557
- | — | 导出面统一:`from kit.db import PgKit, MySqlKit, SQLiteKit, build_pg_url, …` | 纯新增;既有导出未删除/未改名 |
558
-
559
527
  **两段查询范式(推荐用法)**:`search_vectors` 只返回 `[{"id", "distance"}]`,载荷二次取回:
560
528
 
561
529
  ```python
@@ -662,13 +630,15 @@ AuditConfig.set_fields(None) # 恢复默认
662
630
 
663
631
  约定与局限:
664
632
 
633
+ - `set_fields` 的列名必须是合法标识符(字母 / 数字 / 下划线,不以数字开头),
634
+ 否则立即 `ValueError`——列名会被拼进 SQL 的标识符位置。
665
635
  - `deleted` 支持整数列(填 `0`)与字符串列(填 `"0"`,`VARCHAR` / `CHAR` / `TEXT`);
666
636
  **boolean 列不支持**(会报类型错误)。
667
637
  - 查询过滤严格等值 `deleted = 0`(字符串列 `"0"`);`NULL` **不**视为未删除,历史数据需自行回填。
668
638
  - 写方法与 `query()` 手写 SQL 不追加过滤条件;`upsert` 的冲突回查跳过过滤。
669
639
  - `user_resolver` 返回 `None` / 空串或抛异常时跳过 `*_by` 填充,不影响写入。
670
640
 
671
- **原生写工具(0.6.0)**:`raw_insert` / `raw_update` / `raw_touch` / `raw_delete` 自带事务;
641
+ **原生写工具**:`raw_insert` / `raw_update` / `raw_touch` / `raw_delete` 自带事务;
672
642
  `build_raw_*` 只拼装(返回 `(语句, 参数)`),供 `with Kit.transaction() as session:` 内使用:
673
643
 
674
644
  ```python
@@ -941,6 +911,61 @@ CommonKit.format_current_time("yyyy-MM-dd")
941
911
 
942
912
  ---
943
913
 
914
+ ## kit.license_kit —— 离线授权签发与校验
915
+
916
+ 零第三方依赖(`cryptography` 已在 base),按需 import。校验链:签名 → 产品 → 机器码 → 有效期。
917
+
918
+ ```python
919
+ from datetime import datetime, timezone
920
+ from pathlib import Path
921
+
922
+ from kit.license_kit import LicenseKit, LicensePayload, LicenseVerifier
923
+
924
+ # 1. 生成密钥对(一次性;私钥自行保管,不随包分发)
925
+ private_pem, public_pem = LicenseKit.generate_keypair()
926
+
927
+ # 2. 取机器码(16 位十六进制;仅 Windows 支持)
928
+ machine = LicenseKit.get_machine_code()
929
+ LicenseKit.export_fingerprint_file(Path("fingerprint.txt")) # 或导出文件交给签发方
930
+ machine = LicenseKit.parse_fingerprint_file(Path("fingerprint.txt"))
931
+
932
+ # 3. 签发(离线)
933
+ payload = LicensePayload(
934
+ product="thinktank",
935
+ machine_code=machine,
936
+ tier="pro",
937
+ customer_id="C-0001",
938
+ expires_at=datetime(2027, 12, 31, tzinfo=timezone.utc),
939
+ )
940
+ token = LicenseKit.sign(payload, private_pem)
941
+
942
+ # 4. 校验(应用启动 / 请求时)
943
+ verifier = LicenseVerifier(public_pem, Path("license.json"))
944
+ claims = verifier.verify(product="thinktank", machine_code=machine)
945
+ print(claims.tier, claims.expires_at)
946
+ ```
947
+
948
+ | 符号 | 说明 |
949
+ |------|------|
950
+ | `LicenseKit.generate_keypair() -> (str, str)` | 生成 Ed25519(私钥 PEM, 公钥 PEM) |
951
+ | `LicenseKit.get_machine_code() -> str` | 本机机器码(16 位十六进制;⚠ 仅 Windows 平台) |
952
+ | `LicenseKit.export_fingerprint_file(path) -> Path` | 导出机器指纹文件 |
953
+ | `LicenseKit.parse_fingerprint_file(path) -> str` | 从指纹文件读回机器码 |
954
+ | `LicenseKit.sign(payload, private_key_pem) -> str` | 签发授权 token |
955
+ | `LicenseVerifier(public_key_pem, license_path)` | 构造验签器(绑定公钥与 license 文件) |
956
+ | `LicenseVerifier.verify(*, product, machine_code, now=None) -> LicensePayload` | 全量校验并返回载荷;`now=None` 取 aware UTC,naive 按 UTC 解释 |
957
+ | `LicenseVerifier.reverify() -> LicensePayload` | 用最近一次成功 `verify` 的上下文重新校验(文件被换会失败) |
958
+ | `LicenseVerifier.file_signature` | 当前 license 文件的签名(属性) |
959
+
960
+ `LicensePayload` 字段:`version` / `product` / `machine_code` / `tier` / `customer_id` /
961
+ `issued_at` / `expires_at`。
962
+
963
+ 异常:`LicenseError`(基类)、`LicenseFormatError`、`LicenseSignatureError`、
964
+ `LicenseProductMismatchError`、`LicenseMachineMismatchError`、`LicenseExpiredError`、
965
+ `LicenseUnsupportedPlatformError`(非 Windows 取机器码时抛)。
966
+
967
+ ---
968
+
944
969
  ## kit.docs.anydoc_kit —— 文档转 Markdown
945
970
 
946
971
  安装:`pip install "sdpy-kit[anydoc]"`(支持 `.doc .docx .ppt .pptx .xls .xlsx .csv .pdf`)
@@ -4,7 +4,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
4
4
 
5
5
  - **统一数据库访问层**:PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致,切换数据库只改一行 import;内置审计字段自动填充(`created_*` / `updated_*` / `deleted`)与软删除查询过滤;
6
6
  - **AI 多模型服务池**:chat / embedding / rerank / image 四类能力,多厂商候选链 + 故障自动转移;
7
- - **工具集**:Redis / 文件 / 雪花 ID / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / 微信公众号文章抓取存储 / 通用网页文章抓取存储。
7
+ - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
8
8
 
9
9
  > 要求 Python 3.13+
10
10
 
@@ -18,6 +18,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
18
18
  | `kit.redis_kit` | `RedisKit` | Redis string / hash / list / set / zset | `redis` |
19
19
  | `kit.file_kit` | `FileKit` | 文件与目录读写、移动、遍历 | 无 |
20
20
  | `kit.common_kit` | `CommonKit` | 雪花 ID、时间戳 ID、时间格式化 | 无 |
21
+ | `kit.license_kit` | `LicenseKit` / `LicenseVerifier` | Ed25519 离线授权:密钥对、机器码、签发、验签(签名 → 产品 → 机器码 → 有效期) | 无 |
21
22
  | `kit.docs.anydoc_kit` | `AnyDocKit` | docx / pdf / xlsx 等 → Markdown | `anydoc` |
22
23
  | `kit.docs.docx_kit` | `DocxKit` | Word 模板动态填充(章节 / 列表 / 表格) | `docx` |
23
24
  | `kit.docs.rc_docparser_kit` | `RcDocParserKit` | 离线 docx → Markdown + Images(无模型) | `docx-parse` |
@@ -111,7 +112,7 @@ SQLITE_DB_PATH=./data/sqlite.db
111
112
 
112
113
  ### 环境变量 ↔ 能力对照表
113
114
 
114
- > 表中未出现的模块(`kit.crawl.article_models`、`kit.common_kit`、`kit.docs.docx_kit`、`kit.docs.md2doc_kit`、`kit.crawl.wx_article_kit`、`kit.crawl.web_article_kit`)不读取任何环境变量。
115
+ > 表中未出现的模块(`kit.crawl.article_models`、`kit.crawl.article_io`、`kit.common_kit`、`kit.docs.docx_kit`、`kit.docs.md2doc_kit`、`kit.license_kit`)不读取任何环境变量。
115
116
 
116
117
  **基础与日志**(影响 `kit.config` 及所有模块的日志输出)
117
118
 
@@ -196,6 +197,21 @@ SQLITE_DB_PATH=./data/sqlite.db
196
197
  > `LAYA_HF_*` 同样由 kit 在**导入 laya 之前**经 `os.environ.setdefault` 应用,不覆盖显式配置;
197
198
  > `LAYA_MODEL` / `LAYA_DEVICE` 由 `Settings` 读取(进程内缓存,改后需重启进程)。
198
199
 
200
+ **DeepSeek Search / Fetch** → `kit.crawl.deepseek_kit`
201
+
202
+ | 环境变量 | 默认值 | 作用 |
203
+ |----------|--------|------|
204
+ | `DEEPSEEK_API_KEY` | 无 | Search / Fetch 鉴权 Key(与模型对话共用同一套);缺失时调用抛 `DeepSeekKitError` |
205
+ | `DEEPSEEK_BASE_URL` | `https://api.deepseek.com` | API 基础地址 |
206
+ | `DEEPSEEK_TIMEOUT` | `60`(1–600) | 单次 HTTP 请求超时(秒) |
207
+ | `DEEPSEEK_MAX_RETRIES` | `2`(0–5) | 408 / 429 / 5xx 重试次数(不含首次请求);其它 4xx 不重试 |
208
+
209
+ **文章抓取长度自检** → `kit.crawl.wx_article_kit` / `web_article_kit` / `deepseek_kit`
210
+
211
+ | 环境变量 | 默认值 | 作用 |
212
+ |----------|--------|------|
213
+ | `CRAWL_MIN_CONTENT_CHARS` | `500`(≥0) | 正文短于该长度时写入 `warnings`(非致命,不改成功语义、不截断正文);`0` 关闭该判据 |
214
+
199
215
  **云 OCR**(任何 `ocr="hosted"` 的转换)→ `kit.docs.anydoc_kit`
200
216
 
201
217
  | 环境变量 | 默认值 | 作用 |
@@ -293,9 +309,9 @@ from kit import session_scope, transactional, DBError # 等价
293
309
  | 原生表 | `create_raw_table(table_name, columns, if_not_exists=True) -> None` | 按「列名 → 列定义」建普通表;⚠ 列定义须按目标库方言书写,不做方言翻译 |
294
310
  | 原生表 | `raw_insert(table_name, data, *, ignore_conflict=False) -> int` | 插入一行;`ignore_conflict=True` 幂等(PG/SQLite `ON CONFLICT DO NOTHING`、MySQL `INSERT IGNORE`) |
295
311
  | 原生表 | `raw_update(table_name, data, where, params=None) -> int` | 按 `where`(不含 `WHERE`)更新;启用审计填充时自动补 `updated_*` |
296
- | 原生表 | `raw_touch(table_name, where, params=None) -> int` | **0.6.0**:只刷新 `updated_*`(需先 `enable`;表无该列抛 `DBError`) |
297
- | 原生表 | `raw_delete(table_name, where, params=None) -> int` | **0.6.0**:条件删除(`where` 必填) |
298
- | 原生表 | `build_raw_insert / build_raw_update / build_raw_touch / build_raw_delete` | **0.6.0**:只拼装不执行,返回 `(语句, 参数)`,供**自有事务**内 `session.execute(*builder(...))` |
312
+ | 原生表 | `raw_touch(table_name, where, params=None) -> int` | 只刷新 `updated_*`(需先 `enable`;表无该列抛 `DBError`) |
313
+ | 原生表 | `raw_delete(table_name, where, params=None) -> int` | 条件删除(`where` 必填) |
314
+ | 原生表 | `build_raw_insert / build_raw_update / build_raw_touch / build_raw_delete` | 只拼装不执行,返回 `(语句, 参数)`,供**自有事务**内 `session.execute(*builder(...))` |
299
315
  | 原生表 | `raw_get_by(table_name, column, value) -> dict \| None` | 按单列等值取首行 |
300
316
  | 事务 | `transaction()` | 事务上下文管理器,`with PgKit.transaction() as session:` |
301
317
  | 表 | `list_tables() -> list[str]` | 列出全部表名 |
@@ -429,57 +445,9 @@ PgKit.insert_vectors("my_vec", [[0.1] * 384], ids=[1], id_col="vid", column="vec
429
445
  PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
430
446
  ```
431
447
 
432
- #### 0.2.2 行为变更(升级须知)
433
-
434
- | 编号 | 变更 | 影响 |
435
- |------|------|------|
436
- | B1 | `search_vectors` 新增 `exact: bool = True`,默认**精确** | SQLite 返回的 `distance` 由量化值(≈L2×255)变为**真值 L2**;需旧口径请显式 `exact=False` |
437
- | B2 | `filter_sql` 语义统一为**先过滤、再取 top_k** | 传 `filter_sql` 的调用返回值**变多/变全**(旧结果 ⊆ 新结果);曾自行放大 `top_k` 兜底的调用可收紧 |
438
- | B3 | 默认 `distance_type` 由 `"COSINE"` 改为 `"L2"` | 依赖默认值的调用方请显式传 `distance_type="COSINE"` |
439
- | B4 | `create_vector_table` 的 `dims` **必填**,参数顺序为 `(table_name, dims, id_col, column, extra_cols)` | `create_vector_table("t")` 类调用需补维度;位置传 `id_col` 的调用改为关键字 |
440
- | B5 | MySQL 的 `search_vectors` 异常由 `NotImplementedError` 改为 `VectorNotSupportedError`(继承 `DBError`) | `except NotImplementedError` 需改为 `except VectorNotSupportedError`(或 `except DBError`) |
441
-
442
- > 新增(非破坏性):`VECTOR_CAPABILITIES` 能力声明、`id_col` / `column` 逃生口参数、
443
- > `insert_vectors` 支持二维 numpy 数组与批量提交。
444
-
445
- #### 0.2.6 变更
446
-
447
- | 编号 | 变更 | 影响 |
448
- |------|------|------|
449
- | D1 | `prepare_statement` 的 `params` 支持**参数行列表**(`list[dict]`):**原样返回该列表** → `session.execute(*prepare_statement(sql, params))` 即批量执行(executemany),与 `build_insert_vectors` 的返回形态直接对接;行键集不一致或行值含序列 → 本地可操作 `ValueError`(与 `execute` 批量形态**同一规则**) | 向后兼容:单字典 / `None` 形态行为不变;行列表形态**不做** `IN` 展开(与批量互斥) |
450
- | D1 | 文档:修正「自有事务」示例——0.2.5 的示例把 `build_insert_vectors` 返回的**行列表**传给了单字典形态,照抄会抛 `AttributeError: 'list' object has no attribute 'items'` | **纯文档缺陷修正**(0.2.5 示例照抄即崩) |
451
- | — | 文档:批量执行补充 `rowcount` 备注——忽略型写入(如 SQLite `INSERT OR IGNORE`)的冲突行不计入 `rowcount`(即 `rowcount` = 实际插入数;⚠ 仅 SQLite 实测) | 文档 |
452
-
453
- #### 0.2.5 变更
454
-
455
- | 编号 | 变更 | 影响 |
456
- |------|------|------|
457
- | — | 新增**模块级公共函数** `prepare_statement(sql, params=None) -> (语句, 归一化参数)`:供调用方在**自有事务**内执行原生 SQL(含 `IN :ids`),**调用方无须 import `sqlalchemy`**;与 `query` / `execute` 同一实现(单一事实源) | **纯新增**:`query` / `transaction` 的签名与行为**零变更**;共享方法数仍为 **24**(模块级函数不计入) |
458
- | — | `execute` 的 `params` 支持**字典列表**(executemany,同一语句 + N 行参数一次往返);**空列表**不触库返回 `0` | 向后兼容:单字典 / `None` 形态行为不变;批量要求各行字典**键集一致**、行值为**标量**(与 `IN` 展开互斥),违规在本地抛可操作 `ValueError`(含行号),不触库 |
459
- | — | 文档:自有事务示例改用 `prepare_statement`(不再需要 `from sqlalchemy import text`);补「批量执行」说明与约束 | 文档 |
460
-
461
- > ⚠ `:name::type` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 `text()` 识别**(0.2.5 之前亦如此),
448
+ > ⚠ `:name::type` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 `text()` 识别**,
462
449
  > 请改用 `CAST(:name AS type)`。
463
450
 
464
- #### 0.2.4 变更
465
-
466
- | 编号 | 变更 | 影响 |
467
- |------|------|------|
468
- | B4 | **入参自洽性校验**(`extra_data` 的键):保留绑定名 `id` / `vec`、与 `id_col` / `column` 重复、不是合法标识符、`id_col == column` → 抛可操作的 `ValueError` | **唯一行为变更**:此前 SQLite 会**静默写坏数据**(如 `id` 列被覆盖)、PG/MySQL 报误导性错误;正常调用**零影响** |
469
- | — | 新增共享方法 `build_insert_vectors`(只拼装不执行,由 `BaseDbKit` 收敛 → 共享方法 23 → **24**) | 纯新增:可在**自有事务**内写向量行(见下) |
470
- | — | `create_vector_table` 的 `extra_cols` 支持 `{列名: 类型串}`(与 `create_raw_table` 同形) | 纯新增:既有 `[{"name","type"}]` 形态保留,两者产出 DDL 逐字一致 |
471
- | — | 后端差异收口:向量列占位符由 `VECTOR_PLACEHOLDER` 驱动(MySQL = `STRING_TO_VECTOR(:vec)`) | 内部整理:公开签名与行为不变 |
472
- | — | `SQLiteKit.insert_vectors` 的事务获取统一为 `transaction()`(与另两库同构) | 内部一致:行为等价(自成一事务并提交) |
473
-
474
- #### 0.2.3 变更
475
-
476
- | 编号 | 变更 | 影响 |
477
- |------|------|------|
478
- | B6 | **`PgKit.get_table_info` 按数据库解析语义定位表**(`search_path` 解析到的那张表) | 多 schema 下同名表**不再混行**;单 schema / `public` 场景**无影响**;只接受非限定表名 |
479
- | — | `query` / `execute` 支持**列表参数**(`IN :ids`,三库通用) | 纯新增;`set`/`frozenset` 自动归一化为 `list` |
480
- | — | 新增 `SQLiteKit.encode_vector` / `decode_vector`(小端 float32) | 纯新增:可在自有事务内写向量表,见下 |
481
- | — | 导出面统一:`from kit.db import PgKit, MySqlKit, SQLiteKit, build_pg_url, …` | 纯新增;既有导出未删除/未改名 |
482
-
483
451
  **两段查询范式(推荐用法)**:`search_vectors` 只返回 `[{"id", "distance"}]`,载荷二次取回:
484
452
 
485
453
  ```python
@@ -586,13 +554,15 @@ AuditConfig.set_fields(None) # 恢复默认
586
554
 
587
555
  约定与局限:
588
556
 
557
+ - `set_fields` 的列名必须是合法标识符(字母 / 数字 / 下划线,不以数字开头),
558
+ 否则立即 `ValueError`——列名会被拼进 SQL 的标识符位置。
589
559
  - `deleted` 支持整数列(填 `0`)与字符串列(填 `"0"`,`VARCHAR` / `CHAR` / `TEXT`);
590
560
  **boolean 列不支持**(会报类型错误)。
591
561
  - 查询过滤严格等值 `deleted = 0`(字符串列 `"0"`);`NULL` **不**视为未删除,历史数据需自行回填。
592
562
  - 写方法与 `query()` 手写 SQL 不追加过滤条件;`upsert` 的冲突回查跳过过滤。
593
563
  - `user_resolver` 返回 `None` / 空串或抛异常时跳过 `*_by` 填充,不影响写入。
594
564
 
595
- **原生写工具(0.6.0)**:`raw_insert` / `raw_update` / `raw_touch` / `raw_delete` 自带事务;
565
+ **原生写工具**:`raw_insert` / `raw_update` / `raw_touch` / `raw_delete` 自带事务;
596
566
  `build_raw_*` 只拼装(返回 `(语句, 参数)`),供 `with Kit.transaction() as session:` 内使用:
597
567
 
598
568
  ```python
@@ -865,6 +835,61 @@ CommonKit.format_current_time("yyyy-MM-dd")
865
835
 
866
836
  ---
867
837
 
838
+ ## kit.license_kit —— 离线授权签发与校验
839
+
840
+ 零第三方依赖(`cryptography` 已在 base),按需 import。校验链:签名 → 产品 → 机器码 → 有效期。
841
+
842
+ ```python
843
+ from datetime import datetime, timezone
844
+ from pathlib import Path
845
+
846
+ from kit.license_kit import LicenseKit, LicensePayload, LicenseVerifier
847
+
848
+ # 1. 生成密钥对(一次性;私钥自行保管,不随包分发)
849
+ private_pem, public_pem = LicenseKit.generate_keypair()
850
+
851
+ # 2. 取机器码(16 位十六进制;仅 Windows 支持)
852
+ machine = LicenseKit.get_machine_code()
853
+ LicenseKit.export_fingerprint_file(Path("fingerprint.txt")) # 或导出文件交给签发方
854
+ machine = LicenseKit.parse_fingerprint_file(Path("fingerprint.txt"))
855
+
856
+ # 3. 签发(离线)
857
+ payload = LicensePayload(
858
+ product="thinktank",
859
+ machine_code=machine,
860
+ tier="pro",
861
+ customer_id="C-0001",
862
+ expires_at=datetime(2027, 12, 31, tzinfo=timezone.utc),
863
+ )
864
+ token = LicenseKit.sign(payload, private_pem)
865
+
866
+ # 4. 校验(应用启动 / 请求时)
867
+ verifier = LicenseVerifier(public_pem, Path("license.json"))
868
+ claims = verifier.verify(product="thinktank", machine_code=machine)
869
+ print(claims.tier, claims.expires_at)
870
+ ```
871
+
872
+ | 符号 | 说明 |
873
+ |------|------|
874
+ | `LicenseKit.generate_keypair() -> (str, str)` | 生成 Ed25519(私钥 PEM, 公钥 PEM) |
875
+ | `LicenseKit.get_machine_code() -> str` | 本机机器码(16 位十六进制;⚠ 仅 Windows 平台) |
876
+ | `LicenseKit.export_fingerprint_file(path) -> Path` | 导出机器指纹文件 |
877
+ | `LicenseKit.parse_fingerprint_file(path) -> str` | 从指纹文件读回机器码 |
878
+ | `LicenseKit.sign(payload, private_key_pem) -> str` | 签发授权 token |
879
+ | `LicenseVerifier(public_key_pem, license_path)` | 构造验签器(绑定公钥与 license 文件) |
880
+ | `LicenseVerifier.verify(*, product, machine_code, now=None) -> LicensePayload` | 全量校验并返回载荷;`now=None` 取 aware UTC,naive 按 UTC 解释 |
881
+ | `LicenseVerifier.reverify() -> LicensePayload` | 用最近一次成功 `verify` 的上下文重新校验(文件被换会失败) |
882
+ | `LicenseVerifier.file_signature` | 当前 license 文件的签名(属性) |
883
+
884
+ `LicensePayload` 字段:`version` / `product` / `machine_code` / `tier` / `customer_id` /
885
+ `issued_at` / `expires_at`。
886
+
887
+ 异常:`LicenseError`(基类)、`LicenseFormatError`、`LicenseSignatureError`、
888
+ `LicenseProductMismatchError`、`LicenseMachineMismatchError`、`LicenseExpiredError`、
889
+ `LicenseUnsupportedPlatformError`(非 Windows 取机器码时抛)。
890
+
891
+ ---
892
+
868
893
  ## kit.docs.anydoc_kit —— 文档转 Markdown
869
894
 
870
895
  安装:`pip install "sdpy-kit[anydoc]"`(支持 `.doc .docx .ppt .pptx .xls .xlsx .csv .pdf`)
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.6.0"
26
+ __version__ = "0.6.2"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -68,6 +68,7 @@ UPDATE 不参与 空才填 不参与
68
68
  AuditConfig.enable(user_resolver=get_current_userid) # 应用启动时调用一次
69
69
  """
70
70
 
71
+ import re
71
72
  import threading
72
73
  from collections.abc import Callable, Generator, Mapping, MutableMapping
73
74
  from contextlib import contextmanager
@@ -132,6 +133,23 @@ def _is_empty(value: object) -> bool:
132
133
  return isinstance(value, str) and not value.strip()
133
134
 
134
135
 
136
+ # 列名会被拼进 SQL 的标识符位置(`SET name = :name` / `INSERT ... (name)`),
137
+ # 故配置入口必须与 raw_* 的 data 键同一套白名单,避免手误到写入时才报晦涩错误。
138
+ _IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
139
+
140
+
141
+ def _check_field_name(name: object) -> None:
142
+ """校验配置的列名是合法标识符(拼接进 SQL 前拦截)
143
+
144
+ Raises:
145
+ ValueError: 非字符串 / 空串 / 含字母、数字、下划线以外的字符 / 以数字开头
146
+ """
147
+ if not isinstance(name, str) or not _IDENT_RE.match(name):
148
+ raise ValueError(
149
+ f"{name!r} 不是合法列名(仅允许字母、数字、下划线,且不能以数字开头)"
150
+ )
151
+
152
+
135
153
  def _is_string_type(column_type: TypeEngine[Any]) -> bool:
136
154
  """是否为字符串列(兼容 SQLModel 的 ``AutoString`` 这类 ``TypeDecorator``)
137
155
 
@@ -238,7 +256,8 @@ class AuditConfig:
238
256
  ``deleted``
239
257
 
240
258
  Raises:
241
- ValueError: 出现未知槽位(防手误,例如把 ``created_at`` 写成 ``create_at``)
259
+ ValueError: 出现未知槽位(防手误,例如把 ``created_at`` 写成 ``create_at``);
260
+ 或列名不是合法标识符(会被拼进 SQL 的标识符位置)
242
261
 
243
262
  用法::
244
263
 
@@ -254,6 +273,8 @@ class AuditConfig:
254
273
  f"未知的字段槽位: {sorted(unknown)};"
255
274
  f"可用槽位: {sorted(DEFAULT_FIELDS)}"
256
275
  )
276
+ for name in fields.values():
277
+ _check_field_name(name)
257
278
  merged.update(fields)
258
279
  with cls._lock:
259
280
  cls._fields = merged
@@ -365,6 +386,14 @@ def _candidates(slot: str) -> tuple[str, ...]:
365
386
  return (primary, *(alias for alias in aliases if alias != primary))
366
387
 
367
388
 
389
+ def candidate_columns(slot: str) -> tuple[str, ...]:
390
+ """槽位的候选列名(当前配置名 + 内置别名),供错误消息与诊断使用
391
+
392
+ ⚠ 内部符号(不进 ``kit.db.__all__``)。
393
+ """
394
+ return _candidates(slot)
395
+
396
+
368
397
  def insert_only_columns() -> tuple[str, ...]:
369
398
  """只在 INSERT 填充的列(created_at / created_by 槽位的**全部候选**)
370
399
 
@@ -58,7 +58,9 @@ from sqlmodel import Session, SQLModel
58
58
  from kit.common_kit import CommonKit
59
59
  from kit.config.log_config import get_default_logger
60
60
  from kit.db.audit import (
61
+ UPDATED_AT,
61
62
  AuditConfig,
63
+ candidate_columns,
62
64
  deleted_predicate,
63
65
  fill_insert_for,
64
66
  fill_update,
@@ -195,7 +197,15 @@ def _adapt_write_values(
195
197
  data[name] = json.dumps(value, ensure_ascii=False)
196
198
  continue
197
199
  if isinstance(value, _WRITE_SEQUENCE_TYPES):
198
- if isinstance(columns.get(name), JSON):
200
+ column_type = columns.get(name)
201
+ if column_type is None:
202
+ raise ValueError(
203
+ f"{name!r} 的值是序列,但表 {table_name} 中查不到该列"
204
+ f"(或列探测失败):请确认表已提交建好;"
205
+ f"JSON 列请传 dict / list;向量请用 to_vector_literal(...);"
206
+ f"数组等其它类型请用 execute() 手写 SQL"
207
+ )
208
+ if isinstance(column_type, JSON):
199
209
  data[name] = json.dumps(list(value), ensure_ascii=False)
200
210
  else:
201
211
  raise ValueError(
@@ -1000,8 +1010,9 @@ class BaseDbKit(ABC):
1000
1010
  "或改用 raw_update 显式给字段"
1001
1011
  )
1002
1012
  if not touch_columns(cls.DB_TYPE, table_name):
1013
+ expected = " / ".join(candidate_columns(UPDATED_AT))
1003
1014
  raise DBError(
1004
- f"表 {table_name} 没有 updated_at 相关列,无法 touch;"
1015
+ f"表 {table_name} 没有 {expected} 中的任何一列,无法 touch;"
1005
1016
  f"请改用 raw_update 显式给字段"
1006
1017
  )
1007
1018
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes