sdpy-kit 0.6.1__tar.gz → 0.6.3__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.1 → sdpy_kit-0.6.3}/PKG-INFO +107 -59
  2. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/README.md +106 -58
  3. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/__init__.py +1 -1
  4. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/embedding_service.py +57 -1
  5. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/profiles.py +3 -1
  6. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/runner.py +5 -3
  7. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/mysql_kit.py +42 -0
  8. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/pg_kit.py +44 -1
  9. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/sqlite_kit.py +29 -1
  10. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/.gitignore +0 -0
  11. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/__init__.py +0 -0
  12. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/base_service.py +0 -0
  13. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/chat_service.py +0 -0
  14. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/exceptions.py +0 -0
  15. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/failover.py +0 -0
  16. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/image_service.py +0 -0
  17. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/model_builder.py +0 -0
  18. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/model_client.py +0 -0
  19. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/pool.py +0 -0
  20. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/rerank_service.py +0 -0
  21. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/ai/types.py +0 -0
  22. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/common_kit.py +0 -0
  23. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/config/__init__.py +0 -0
  24. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/config/kit_config.py +0 -0
  25. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/config/log_config.py +0 -0
  26. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/crawl/__init__.py +0 -0
  27. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/crawl/article_io.py +0 -0
  28. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/crawl/article_models.py +0 -0
  29. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/crawl/deepseek_kit.py +0 -0
  30. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/crawl/web_article_kit.py +0 -0
  31. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/crawl/wx_article_kit.py +0 -0
  32. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/__init__.py +0 -0
  33. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/audit.py +0 -0
  34. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/base_db_kit.py +0 -0
  35. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/crud.py +0 -0
  36. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/engine.py +0 -0
  37. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/exceptions.py +0 -0
  38. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/models/__init__.py +0 -0
  39. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/models/base.py +0 -0
  40. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/query.py +0 -0
  41. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/session.py +0 -0
  42. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/db/transaction.py +0 -0
  43. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/docs/__init__.py +0 -0
  44. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/docs/anydoc_kit.py +0 -0
  45. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/docs/docx_kit.py +0 -0
  46. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/docs/marker_kit.py +0 -0
  47. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/docs/md2doc_kit.py +0 -0
  48. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/docs/mineru_kit.py +0 -0
  49. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/docs/rc_docparser_kit.py +0 -0
  50. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/file_kit.py +0 -0
  51. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/laya_kit.py +0 -0
  52. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/license_kit.py +0 -0
  53. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/kit/redis_kit.py +0 -0
  54. {sdpy_kit-0.6.1 → sdpy_kit-0.6.3}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sdpy-kit
3
- Version: 0.6.1
3
+ Version: 0.6.3
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]` | 列出全部表名 |
@@ -435,6 +451,7 @@ with SQLiteKit.transaction() as session: # 多操作原子提
435
451
  | `insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> int` | 批量插入向量(**自成一事务并提交**);`ids=None` 时由 Kit 生成雪花 ID;接受 `list[list[float]]` 或二维 numpy 数组 |
436
452
  | `build_insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> list[tuple[str, list[dict]]]` | **只拼装、不执行、不管事务**:返回按列集分组的 `(SQL, 参数列表)`,供调用方在**自有事务**内写向量行(见「自有事务内写向量」) |
437
453
  | `search_vectors(table_name, query_vector, top_k=10, distance_type="L2", filter_sql=None, exact=True, id_col="id", column="embedding") -> list[dict]` | KNN 检索,按距离升序返回 `{"id", "distance"}`;`exact=True`(默认)禁止近似加速;**MySQL 侧抛 `VectorNotSupportedError`** |
454
+ | `vector_column_dimension(table_name, column="embedding") -> int \| None` | 查列**声明的维度**:PG 为精确维度(`vector(1024)`→1024);MySQL 为**最大上限**(未指定维度落 `2048`);SQLite 恒 `None`(无元数据)。列不存在 / 非向量列 / 无维度声明 → `None`,不抛异常。口径差异由 `VECTOR_CAPABILITIES["dimension_kind"]` 声明(`"exact"` / `"max"` / `None`) |
438
455
  | `vector_version() -> str` | 向量后端版本;不可用时返回 `"unavailable"`(MySQL 侧为服务端版本号) |
439
456
 
440
457
  **`id_col` / `column`(逃生口)**:让**自建 DDL 的向量表**也能读写/检索。需要复合主键、额外约束、
@@ -457,12 +474,17 @@ with SQLiteKit.transaction() as session: # 多操作原子提
457
474
  | 默认 `distance_type` | `"L2"` | `"L2"`(三库统一,不按后端覆写) | `"L2"`(形参保留,调用即抛错) |
458
475
  | 非法取值 | 抛 `ValueError` | 抛 `ValueError` | `search_vectors` 抛 `VectorNotSupportedError` |
459
476
  | `distance` 口径 | `exact=True` 真值 L2 / `exact=False` 量化(×255) | 真实距离 | — |
460
- | 维度约束 | `CHECK`(仅新表;旧表靠方法内校验) | 强制(不匹配即报错;建表时回读比对) | 上限约束(超限报错) |
477
+ | 维度约束 | `CHECK`(仅新表;旧表靠方法内校验) | 强制(不匹配即报错;建表时回读比对) | **上限**(`VECTOR(N)` 的 N 是最大维度,写入须 ≤ N,超限报 `1406 Data too long`) |
478
+ | 维度查询 | 恒 `None`(无元数据) | 精确维度(`dimension_kind="exact"`) | 最大上限(`dimension_kind="max"`) |
461
479
  | `filter_sql` 与 `top_k` | **先过滤再取 top_k**(与 PG 一致) | **先过滤再取 top_k** | 不适用 |
462
480
  | 索引 | 无(精确路径为全表扫描) | kit 不建索引;**自建后须 `exact=False` 才会被使用** | 无法建(B-Tree 拒绝向量列) |
463
481
 
464
482
  > MySQL `DISTANCE()` / `VECTOR_DISTANCE()` 仅 HeatWave on OCI 与 MySQL AI 提供
465
483
  > (Community 实测报 `1305 FUNCTION ... does not exist`);故 `MySqlKit` 只覆盖存储侧。
484
+ >
485
+ > 维度上限(插件侧实测 pgvector 0.8.7):`vector` ≤ **16000** 维;hnsw / ivfflat 索引
486
+ > ≤ **2000** 维;`halfvec` 表达式索引 ≤ **4000** 维。因此 **1024 维表可建 HNSW,
487
+ > 2560 / 4096 维表建不了**。
466
488
 
467
489
  **主键与 id(三库统一)**:向量表主键均为 `BIGINT`(SQLite 用等价的 64 位 `INTEGER PRIMARY KEY`),
468
490
  **不使用数据库自增**;`ids=None` 时由 Kit 用 `CommonKit.generate_snowflake_id()` 生成雪花 ID
@@ -497,7 +519,11 @@ MySqlKit.insert_vectors("images", [[0.1] * 384, [0.2] * 384])
497
519
  PgKit.VECTOR_CAPABILITIES
498
520
  # {'store': True, 'search': True, 'exact': True,
499
521
  # 'distances': ('COSINE', 'L2', 'L1', 'DOT', 'SQUARED_L2'),
500
- # 'distance_kind': 'true', 'fast_distance_kind': 'true'}
522
+ # 'distance_kind': 'true', 'fast_distance_kind': 'true',
523
+ # 'dimension_kind': 'exact'} # MySQL='max' / SQLite=None
524
+
525
+ # 查询库列声明的维度(守卫「配置 vs 库列」用;查不到返回 None)
526
+ PgKit.vector_column_dimension("images", "embedding") # 384
501
527
 
502
528
  # 自建 DDL 的表:用 id_col / column 接入
503
529
  PgKit.execute("CREATE TABLE my_vec (vid BIGINT PRIMARY KEY, vec vector(384))")
@@ -505,57 +531,9 @@ PgKit.insert_vectors("my_vec", [[0.1] * 384], ids=[1], id_col="vid", column="vec
505
531
  PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
506
532
  ```
507
533
 
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 之前亦如此),
534
+ > ⚠ `:name::type` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 `text()` 识别**,
538
535
  > 请改用 `CAST(:name AS type)`。
539
536
 
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
537
  **两段查询范式(推荐用法)**:`search_vectors` 只返回 `[{"id", "distance"}]`,载荷二次取回:
560
538
 
561
539
  ```python
@@ -670,7 +648,7 @@ AuditConfig.set_fields(None) # 恢复默认
670
648
  - 写方法与 `query()` 手写 SQL 不追加过滤条件;`upsert` 的冲突回查跳过过滤。
671
649
  - `user_resolver` 返回 `None` / 空串或抛异常时跳过 `*_by` 填充,不影响写入。
672
650
 
673
- **原生写工具(0.6.0)**:`raw_insert` / `raw_update` / `raw_touch` / `raw_delete` 自带事务;
651
+ **原生写工具**:`raw_insert` / `raw_update` / `raw_touch` / `raw_delete` 自带事务;
674
652
  `build_raw_*` 只拼装(返回 `(语句, 参数)`),供 `with Kit.transaction() as session:` 内使用:
675
653
 
676
654
  ```python
@@ -844,12 +822,27 @@ await service.close()
844
822
  | 类 | 方法 |
845
823
  |----|------|
846
824
  | `ChatService` | `ask(messages, *, temperature=None, max_tokens=None, deadline=None) -> str`、`complete(prompt, *, ...) -> str`、`stream(messages, *, ...) -> AsyncIterator[str]` |
847
- | `EmbeddingService` | `embed_text(text) -> list[float]`、`embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]`、`dimension` 属性 |
825
+ | `EmbeddingService` | `embed_text(text) -> list[float]`、`embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]`、`dimension` 属性(**目标维度**,见下) |
848
826
  | `RerankService` | `rerank(query, documents, *, top_n=None) -> list[RerankHit]`(已按 score 降序) |
849
827
  | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> str` |
850
828
  | `ModelBuilder` | `build(model_type, model=None, *, api_key=None)` 及各类型专用 `build_chat / build_embedding / build_rerank / build_image` |
851
829
  | `BaseAIService` | `profile` 属性、`await close()`、支持 `async with` |
852
830
 
831
+ **embedding 维度契约**:`ai_models.json` 里 embedding 档案的 `dimension` 是**目标向量维度**
832
+ (不是模型原生维度),`EmbeddingService` 保证输出恒等于它:
833
+
834
+ | 模型返回维 vs `dimension` | 行为 |
835
+ |---|---|
836
+ | 相等 | 原样返回(不归一化,逐字节不变) |
837
+ | 更大 | **截断到该维度并做 L2 归一化**(依赖模型的 MRL 支持;非 MRL 模型截断后质量会退化) |
838
+ | 更小 | 抛 `AIResponseError`(降维只能变小,不能放大) |
839
+
840
+ 用途:让「建表维度 = 配置维度 = 输出维度」共用一个数值——
841
+ `create_vector_table(t, dims)` 与 `vector_column_dimension(t, col)` 都取该配置值,
842
+ 换用原生维度更大的模型时无需重构向量列(换模型后需全量重新入库)。
843
+
844
+ > ⚠ 启用候选链时,**链内各档案的 `dimension` 必须一致**:故障切换后输出维度跟随被选中档案的配置。
845
+
853
846
  ### 3. 异常
854
847
 
855
848
  ```python
@@ -943,6 +936,61 @@ CommonKit.format_current_time("yyyy-MM-dd")
943
936
 
944
937
  ---
945
938
 
939
+ ## kit.license_kit —— 离线授权签发与校验
940
+
941
+ 零第三方依赖(`cryptography` 已在 base),按需 import。校验链:签名 → 产品 → 机器码 → 有效期。
942
+
943
+ ```python
944
+ from datetime import datetime, timezone
945
+ from pathlib import Path
946
+
947
+ from kit.license_kit import LicenseKit, LicensePayload, LicenseVerifier
948
+
949
+ # 1. 生成密钥对(一次性;私钥自行保管,不随包分发)
950
+ private_pem, public_pem = LicenseKit.generate_keypair()
951
+
952
+ # 2. 取机器码(16 位十六进制;仅 Windows 支持)
953
+ machine = LicenseKit.get_machine_code()
954
+ LicenseKit.export_fingerprint_file(Path("fingerprint.txt")) # 或导出文件交给签发方
955
+ machine = LicenseKit.parse_fingerprint_file(Path("fingerprint.txt"))
956
+
957
+ # 3. 签发(离线)
958
+ payload = LicensePayload(
959
+ product="thinktank",
960
+ machine_code=machine,
961
+ tier="pro",
962
+ customer_id="C-0001",
963
+ expires_at=datetime(2027, 12, 31, tzinfo=timezone.utc),
964
+ )
965
+ token = LicenseKit.sign(payload, private_pem)
966
+
967
+ # 4. 校验(应用启动 / 请求时)
968
+ verifier = LicenseVerifier(public_pem, Path("license.json"))
969
+ claims = verifier.verify(product="thinktank", machine_code=machine)
970
+ print(claims.tier, claims.expires_at)
971
+ ```
972
+
973
+ | 符号 | 说明 |
974
+ |------|------|
975
+ | `LicenseKit.generate_keypair() -> (str, str)` | 生成 Ed25519(私钥 PEM, 公钥 PEM) |
976
+ | `LicenseKit.get_machine_code() -> str` | 本机机器码(16 位十六进制;⚠ 仅 Windows 平台) |
977
+ | `LicenseKit.export_fingerprint_file(path) -> Path` | 导出机器指纹文件 |
978
+ | `LicenseKit.parse_fingerprint_file(path) -> str` | 从指纹文件读回机器码 |
979
+ | `LicenseKit.sign(payload, private_key_pem) -> str` | 签发授权 token |
980
+ | `LicenseVerifier(public_key_pem, license_path)` | 构造验签器(绑定公钥与 license 文件) |
981
+ | `LicenseVerifier.verify(*, product, machine_code, now=None) -> LicensePayload` | 全量校验并返回载荷;`now=None` 取 aware UTC,naive 按 UTC 解释 |
982
+ | `LicenseVerifier.reverify() -> LicensePayload` | 用最近一次成功 `verify` 的上下文重新校验(文件被换会失败) |
983
+ | `LicenseVerifier.file_signature` | 当前 license 文件的签名(属性) |
984
+
985
+ `LicensePayload` 字段:`version` / `product` / `machine_code` / `tier` / `customer_id` /
986
+ `issued_at` / `expires_at`。
987
+
988
+ 异常:`LicenseError`(基类)、`LicenseFormatError`、`LicenseSignatureError`、
989
+ `LicenseProductMismatchError`、`LicenseMachineMismatchError`、`LicenseExpiredError`、
990
+ `LicenseUnsupportedPlatformError`(非 Windows 取机器码时抛)。
991
+
992
+ ---
993
+
946
994
  ## kit.docs.anydoc_kit —— 文档转 Markdown
947
995
 
948
996
  安装:`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]` | 列出全部表名 |
@@ -359,6 +375,7 @@ with SQLiteKit.transaction() as session: # 多操作原子提
359
375
  | `insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> int` | 批量插入向量(**自成一事务并提交**);`ids=None` 时由 Kit 生成雪花 ID;接受 `list[list[float]]` 或二维 numpy 数组 |
360
376
  | `build_insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> list[tuple[str, list[dict]]]` | **只拼装、不执行、不管事务**:返回按列集分组的 `(SQL, 参数列表)`,供调用方在**自有事务**内写向量行(见「自有事务内写向量」) |
361
377
  | `search_vectors(table_name, query_vector, top_k=10, distance_type="L2", filter_sql=None, exact=True, id_col="id", column="embedding") -> list[dict]` | KNN 检索,按距离升序返回 `{"id", "distance"}`;`exact=True`(默认)禁止近似加速;**MySQL 侧抛 `VectorNotSupportedError`** |
378
+ | `vector_column_dimension(table_name, column="embedding") -> int \| None` | 查列**声明的维度**:PG 为精确维度(`vector(1024)`→1024);MySQL 为**最大上限**(未指定维度落 `2048`);SQLite 恒 `None`(无元数据)。列不存在 / 非向量列 / 无维度声明 → `None`,不抛异常。口径差异由 `VECTOR_CAPABILITIES["dimension_kind"]` 声明(`"exact"` / `"max"` / `None`) |
362
379
  | `vector_version() -> str` | 向量后端版本;不可用时返回 `"unavailable"`(MySQL 侧为服务端版本号) |
363
380
 
364
381
  **`id_col` / `column`(逃生口)**:让**自建 DDL 的向量表**也能读写/检索。需要复合主键、额外约束、
@@ -381,12 +398,17 @@ with SQLiteKit.transaction() as session: # 多操作原子提
381
398
  | 默认 `distance_type` | `"L2"` | `"L2"`(三库统一,不按后端覆写) | `"L2"`(形参保留,调用即抛错) |
382
399
  | 非法取值 | 抛 `ValueError` | 抛 `ValueError` | `search_vectors` 抛 `VectorNotSupportedError` |
383
400
  | `distance` 口径 | `exact=True` 真值 L2 / `exact=False` 量化(×255) | 真实距离 | — |
384
- | 维度约束 | `CHECK`(仅新表;旧表靠方法内校验) | 强制(不匹配即报错;建表时回读比对) | 上限约束(超限报错) |
401
+ | 维度约束 | `CHECK`(仅新表;旧表靠方法内校验) | 强制(不匹配即报错;建表时回读比对) | **上限**(`VECTOR(N)` 的 N 是最大维度,写入须 ≤ N,超限报 `1406 Data too long`) |
402
+ | 维度查询 | 恒 `None`(无元数据) | 精确维度(`dimension_kind="exact"`) | 最大上限(`dimension_kind="max"`) |
385
403
  | `filter_sql` 与 `top_k` | **先过滤再取 top_k**(与 PG 一致) | **先过滤再取 top_k** | 不适用 |
386
404
  | 索引 | 无(精确路径为全表扫描) | kit 不建索引;**自建后须 `exact=False` 才会被使用** | 无法建(B-Tree 拒绝向量列) |
387
405
 
388
406
  > MySQL `DISTANCE()` / `VECTOR_DISTANCE()` 仅 HeatWave on OCI 与 MySQL AI 提供
389
407
  > (Community 实测报 `1305 FUNCTION ... does not exist`);故 `MySqlKit` 只覆盖存储侧。
408
+ >
409
+ > 维度上限(插件侧实测 pgvector 0.8.7):`vector` ≤ **16000** 维;hnsw / ivfflat 索引
410
+ > ≤ **2000** 维;`halfvec` 表达式索引 ≤ **4000** 维。因此 **1024 维表可建 HNSW,
411
+ > 2560 / 4096 维表建不了**。
390
412
 
391
413
  **主键与 id(三库统一)**:向量表主键均为 `BIGINT`(SQLite 用等价的 64 位 `INTEGER PRIMARY KEY`),
392
414
  **不使用数据库自增**;`ids=None` 时由 Kit 用 `CommonKit.generate_snowflake_id()` 生成雪花 ID
@@ -421,7 +443,11 @@ MySqlKit.insert_vectors("images", [[0.1] * 384, [0.2] * 384])
421
443
  PgKit.VECTOR_CAPABILITIES
422
444
  # {'store': True, 'search': True, 'exact': True,
423
445
  # 'distances': ('COSINE', 'L2', 'L1', 'DOT', 'SQUARED_L2'),
424
- # 'distance_kind': 'true', 'fast_distance_kind': 'true'}
446
+ # 'distance_kind': 'true', 'fast_distance_kind': 'true',
447
+ # 'dimension_kind': 'exact'} # MySQL='max' / SQLite=None
448
+
449
+ # 查询库列声明的维度(守卫「配置 vs 库列」用;查不到返回 None)
450
+ PgKit.vector_column_dimension("images", "embedding") # 384
425
451
 
426
452
  # 自建 DDL 的表:用 id_col / column 接入
427
453
  PgKit.execute("CREATE TABLE my_vec (vid BIGINT PRIMARY KEY, vec vector(384))")
@@ -429,57 +455,9 @@ PgKit.insert_vectors("my_vec", [[0.1] * 384], ids=[1], id_col="vid", column="vec
429
455
  PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
430
456
  ```
431
457
 
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 之前亦如此),
458
+ > ⚠ `:name::type` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 `text()` 识别**,
462
459
  > 请改用 `CAST(:name AS type)`。
463
460
 
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
461
  **两段查询范式(推荐用法)**:`search_vectors` 只返回 `[{"id", "distance"}]`,载荷二次取回:
484
462
 
485
463
  ```python
@@ -594,7 +572,7 @@ AuditConfig.set_fields(None) # 恢复默认
594
572
  - 写方法与 `query()` 手写 SQL 不追加过滤条件;`upsert` 的冲突回查跳过过滤。
595
573
  - `user_resolver` 返回 `None` / 空串或抛异常时跳过 `*_by` 填充,不影响写入。
596
574
 
597
- **原生写工具(0.6.0)**:`raw_insert` / `raw_update` / `raw_touch` / `raw_delete` 自带事务;
575
+ **原生写工具**:`raw_insert` / `raw_update` / `raw_touch` / `raw_delete` 自带事务;
598
576
  `build_raw_*` 只拼装(返回 `(语句, 参数)`),供 `with Kit.transaction() as session:` 内使用:
599
577
 
600
578
  ```python
@@ -768,12 +746,27 @@ await service.close()
768
746
  | 类 | 方法 |
769
747
  |----|------|
770
748
  | `ChatService` | `ask(messages, *, temperature=None, max_tokens=None, deadline=None) -> str`、`complete(prompt, *, ...) -> str`、`stream(messages, *, ...) -> AsyncIterator[str]` |
771
- | `EmbeddingService` | `embed_text(text) -> list[float]`、`embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]`、`dimension` 属性 |
749
+ | `EmbeddingService` | `embed_text(text) -> list[float]`、`embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]`、`dimension` 属性(**目标维度**,见下) |
772
750
  | `RerankService` | `rerank(query, documents, *, top_n=None) -> list[RerankHit]`(已按 score 降序) |
773
751
  | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> str` |
774
752
  | `ModelBuilder` | `build(model_type, model=None, *, api_key=None)` 及各类型专用 `build_chat / build_embedding / build_rerank / build_image` |
775
753
  | `BaseAIService` | `profile` 属性、`await close()`、支持 `async with` |
776
754
 
755
+ **embedding 维度契约**:`ai_models.json` 里 embedding 档案的 `dimension` 是**目标向量维度**
756
+ (不是模型原生维度),`EmbeddingService` 保证输出恒等于它:
757
+
758
+ | 模型返回维 vs `dimension` | 行为 |
759
+ |---|---|
760
+ | 相等 | 原样返回(不归一化,逐字节不变) |
761
+ | 更大 | **截断到该维度并做 L2 归一化**(依赖模型的 MRL 支持;非 MRL 模型截断后质量会退化) |
762
+ | 更小 | 抛 `AIResponseError`(降维只能变小,不能放大) |
763
+
764
+ 用途:让「建表维度 = 配置维度 = 输出维度」共用一个数值——
765
+ `create_vector_table(t, dims)` 与 `vector_column_dimension(t, col)` 都取该配置值,
766
+ 换用原生维度更大的模型时无需重构向量列(换模型后需全量重新入库)。
767
+
768
+ > ⚠ 启用候选链时,**链内各档案的 `dimension` 必须一致**:故障切换后输出维度跟随被选中档案的配置。
769
+
777
770
  ### 3. 异常
778
771
 
779
772
  ```python
@@ -867,6 +860,61 @@ CommonKit.format_current_time("yyyy-MM-dd")
867
860
 
868
861
  ---
869
862
 
863
+ ## kit.license_kit —— 离线授权签发与校验
864
+
865
+ 零第三方依赖(`cryptography` 已在 base),按需 import。校验链:签名 → 产品 → 机器码 → 有效期。
866
+
867
+ ```python
868
+ from datetime import datetime, timezone
869
+ from pathlib import Path
870
+
871
+ from kit.license_kit import LicenseKit, LicensePayload, LicenseVerifier
872
+
873
+ # 1. 生成密钥对(一次性;私钥自行保管,不随包分发)
874
+ private_pem, public_pem = LicenseKit.generate_keypair()
875
+
876
+ # 2. 取机器码(16 位十六进制;仅 Windows 支持)
877
+ machine = LicenseKit.get_machine_code()
878
+ LicenseKit.export_fingerprint_file(Path("fingerprint.txt")) # 或导出文件交给签发方
879
+ machine = LicenseKit.parse_fingerprint_file(Path("fingerprint.txt"))
880
+
881
+ # 3. 签发(离线)
882
+ payload = LicensePayload(
883
+ product="thinktank",
884
+ machine_code=machine,
885
+ tier="pro",
886
+ customer_id="C-0001",
887
+ expires_at=datetime(2027, 12, 31, tzinfo=timezone.utc),
888
+ )
889
+ token = LicenseKit.sign(payload, private_pem)
890
+
891
+ # 4. 校验(应用启动 / 请求时)
892
+ verifier = LicenseVerifier(public_pem, Path("license.json"))
893
+ claims = verifier.verify(product="thinktank", machine_code=machine)
894
+ print(claims.tier, claims.expires_at)
895
+ ```
896
+
897
+ | 符号 | 说明 |
898
+ |------|------|
899
+ | `LicenseKit.generate_keypair() -> (str, str)` | 生成 Ed25519(私钥 PEM, 公钥 PEM) |
900
+ | `LicenseKit.get_machine_code() -> str` | 本机机器码(16 位十六进制;⚠ 仅 Windows 平台) |
901
+ | `LicenseKit.export_fingerprint_file(path) -> Path` | 导出机器指纹文件 |
902
+ | `LicenseKit.parse_fingerprint_file(path) -> str` | 从指纹文件读回机器码 |
903
+ | `LicenseKit.sign(payload, private_key_pem) -> str` | 签发授权 token |
904
+ | `LicenseVerifier(public_key_pem, license_path)` | 构造验签器(绑定公钥与 license 文件) |
905
+ | `LicenseVerifier.verify(*, product, machine_code, now=None) -> LicensePayload` | 全量校验并返回载荷;`now=None` 取 aware UTC,naive 按 UTC 解释 |
906
+ | `LicenseVerifier.reverify() -> LicensePayload` | 用最近一次成功 `verify` 的上下文重新校验(文件被换会失败) |
907
+ | `LicenseVerifier.file_signature` | 当前 license 文件的签名(属性) |
908
+
909
+ `LicensePayload` 字段:`version` / `product` / `machine_code` / `tier` / `customer_id` /
910
+ `issued_at` / `expires_at`。
911
+
912
+ 异常:`LicenseError`(基类)、`LicenseFormatError`、`LicenseSignatureError`、
913
+ `LicenseProductMismatchError`、`LicenseMachineMismatchError`、`LicenseExpiredError`、
914
+ `LicenseUnsupportedPlatformError`(非 Windows 取机器码时抛)。
915
+
916
+ ---
917
+
870
918
  ## kit.docs.anydoc_kit —— 文档转 Markdown
871
919
 
872
920
  安装:`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.1"
26
+ __version__ = "0.6.3"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -19,6 +19,7 @@ Author: Clark Chang
19
19
 
20
20
  import asyncio
21
21
  import itertools
22
+ import math
22
23
  import time
23
24
  from dataclasses import dataclass
24
25
  from typing import cast, override
@@ -48,12 +49,28 @@ class EmbeddingService(BaseAIService[EmbeddingRequest, list[list[float]]]):
48
49
 
49
50
  公网 API 有限流限速前提,通过分批 + 可选并发(信号量)控制压力。
50
51
  信号量在运行中的事件循环内惰性创建,避免构造期绑定循环的隐患。
52
+
53
+ **维度契约**:``profile.dimension`` 是**目标向量维度**(不是「模型原生维度」),
54
+ 本服务保证输出恒等于它:
55
+
56
+ - 模型返回维 == ``dimension`` → 原样返回(零计算,逐字节不变);
57
+ - 模型返回维 > ``dimension`` → **截断到该维度并做 L2 归一化**(依赖模型的 MRL
58
+ 支持;非 MRL 模型截断后质量会退化);
59
+ - 模型返回维 < ``dimension`` → 抛 `AIResponseError`(降维只能变小,不能放大)。
60
+
61
+ 这样「建表维度 = 配置维度 = 输出维度」可由同一个配置值统一,
62
+ 换模型(原生维度更大)时无需重构向量列。
63
+
64
+ ⚠ 候选链内各档案的 ``dimension`` 必须一致:故障切换后输出维度跟随
65
+ 被选中档案的配置,不一致会导致同一业务表写入不同维度的向量。
51
66
  """
52
67
 
53
68
  def __init__(self, profile: ModelProfile) -> None:
54
69
  super().__init__(profile)
55
70
  if profile.dimension is None:
56
71
  raise ValueError(f"模型 {profile.model} 未配置 dimension,无法用于向量化")
72
+ # 仅用于「截断只提示一次」,不参与任何行为分支
73
+ self._truncate_logged = False
57
74
 
58
75
  @property
59
76
  def dimension(self) -> int:
@@ -62,6 +79,45 @@ class EmbeddingService(BaseAIService[EmbeddingRequest, list[list[float]]]):
62
79
  raise AIResponseError("dimension 在构造期已校验非空,不应缺失")
63
80
  return dim
64
81
 
82
+ def _fit_dimension(self, vector: list[float]) -> list[float]:
83
+ """把模型输出对齐到配置的目标维度
84
+
85
+ Args:
86
+ vector: 模型原始输出的向量
87
+
88
+ Returns:
89
+ 目标维度的向量:等长时原样返回;超长时截断并 L2 归一化
90
+
91
+ Raises:
92
+ AIResponseError: 输出维度**不足**配置维度(无法放大),
93
+ 或截断后前 N 维全为 0(无法归一化)
94
+ """
95
+ target = self.dimension
96
+ if len(vector) == target:
97
+ return vector
98
+ if len(vector) < target:
99
+ raise AIResponseError(
100
+ f"向量维度不足:模型 {self._profile.model} 返回 {len(vector)} 维,"
101
+ f"配置 dimension={target}。降维只能变小不能放大,"
102
+ f"请改用原生维度 >= {target} 的模型,"
103
+ f"或调低 dimension 并重建向量表(表内既有向量需重新入库)。"
104
+ )
105
+
106
+ head = vector[:target]
107
+ norm = math.sqrt(sum(value * value for value in head))
108
+ if norm == 0.0:
109
+ raise AIResponseError(
110
+ f"模型 {self._profile.model} 返回向量的前 {target} 维全为 0,"
111
+ f"无法按配置 dimension={target} 归一化;请检查模型输出是否正常。"
112
+ )
113
+ if not self._truncate_logged:
114
+ logger.info(
115
+ f"向量已按配置截断:{len(vector)} → {target} 维(含 L2 归一化)。"
116
+ f"截断依赖模型的 MRL 支持,非 MRL 模型质量会退化"
117
+ )
118
+ self._truncate_logged = True
119
+ return [value / norm for value in head]
120
+
65
121
  @override
66
122
  def _build_payload(self, request: EmbeddingRequest) -> JSONObject:
67
123
  return cast(
@@ -92,7 +148,7 @@ class EmbeddingService(BaseAIService[EmbeddingRequest, list[list[float]]]):
92
148
  f"向量化响应 embedding 元素非法: {str(item)[:200]}"
93
149
  )
94
150
  floats.append(float(value))
95
- vectors.append(floats)
151
+ vectors.append(self._fit_dimension(floats))
96
152
  return vectors
97
153
 
98
154
  async def embed_text(self, text: str) -> list[float]:
@@ -71,7 +71,9 @@ class ModelProfile:
71
71
  timeout: 请求超时秒数
72
72
  max_tokens: 单次请求输出上限(chat 用;None 表示由 provider 默认值决定)
73
73
  retries: 5xx 重试次数(超时不重试,候选链切换才是有效重试)
74
- dimension: 向量维度(仅 embedding 有效)
74
+ dimension: **目标**向量维度(仅 embedding 有效)。`EmbeddingService`
75
+ 保证输出恒等于该值:模型原生维度更大时截断(含 L2 归一化),
76
+ 更小时报错;建表与维度守卫均应以它为唯一来源
75
77
  batch_size: 单次请求最大文本数(仅 embedding 有效)
76
78
  """
77
79
 
@@ -784,9 +784,11 @@ class ReloadResult:
784
784
  Attributes:
785
785
  types: 实际重建的模型类型(全量重载为 4 类型的排序列表)
786
786
  embedding_model_changed: embedding 默认档案的 ``model`` 或
787
- ``dimension`` 是否变化。已建向量库的维度与存量向量不会随
788
- 重载迁移,变化时调用方应评估是否需要重新入库
789
- (kit 只上报不拦截——新建库场景下更换 embedding 是合法操作)
787
+ ``dimension`` 是否变化。``dimension`` 是**目标向量维度**
788
+ (`EmbeddingService` 保证输出等于它),故它变化会直接改变
789
+ 输出维度;已建向量库的维度与存量向量不会随重载迁移,
790
+ 变化时调用方应评估是否需要重新入库(kit 只上报不拦截——
791
+ 新建库场景下更换 embedding 是合法操作)
790
792
  previous_embedding_model: 重载前 embedding 默认档案的 model
791
793
  current_embedding_model: 重载后 embedding 默认档案的 model
792
794
  """
@@ -59,6 +59,8 @@ from __future__ import annotations
59
59
  - ``VECTOR_CAPABILITIES`` 已声明上述能力,调用方可 feature-detect 而无需 try/except。
60
60
  """
61
61
 
62
+ import re
63
+
62
64
  from sqlalchemy import text
63
65
 
64
66
  from kit.config.log_config import get_default_logger
@@ -96,6 +98,7 @@ class MySqlKit(BaseDbKit):
96
98
  "distances": (),
97
99
  "distance_kind": None,
98
100
  "fast_distance_kind": None,
101
+ "dimension_kind": "max", # VECTOR(N) 的 N 是「最大维度上限」,非必须相等
99
102
  }
100
103
 
101
104
  @classmethod
@@ -124,6 +127,45 @@ class MySqlKit(BaseDbKit):
124
127
 
125
128
  # ==================== 向量操作(MySQL 9.x 原生 VECTOR 类型) ====================
126
129
 
130
+ @classmethod
131
+ def vector_column_dimension(
132
+ cls, table_name: str, column: str = "embedding"
133
+ ) -> int | None:
134
+ """查询向量列的**声明维度上限**(MySQL 9.x 原生 ``VECTOR``)
135
+
136
+ Args:
137
+ table_name: 表名(当前连接的库内,即 ``DATABASE()``)
138
+ column: 向量列名(默认 ``embedding``)
139
+
140
+ Returns:
141
+ 列声明的维度上限;列不存在或不是 ``VECTOR`` 类型时返回 ``None``
142
+
143
+ Note:
144
+ - MySQL 的 ``VECTOR(N)`` 是**上限语义**:写入维度 ≤ N 即可(实测 9.7.2:
145
+ ``VECTOR(1024)`` 写入 1024 维成功,写入 1025 维报 ``1406 Data too long``)。
146
+ 故 ``VECTOR_CAPABILITIES["dimension_kind"] == "max"``,
147
+ **不能**用「配置维度 != 列值」判定不一致。
148
+ - 建表时未指定维度的 ``VECTOR`` 由服务端落为 ``vector(2048)``(实测 9.7.2),
149
+ 因此本后端不存在「无维度」列,``None`` 只表示「列不是向量列」。
150
+ - 维度元数据只在 ``information_schema.COLUMNS.COLUMN_TYPE`` 里(形如
151
+ ``vector(1024)``),需按文本解析。
152
+ """
153
+ rows = cls.query(
154
+ "SELECT COLUMN_TYPE AS column_type "
155
+ "FROM information_schema.COLUMNS "
156
+ "WHERE TABLE_SCHEMA = DATABASE() "
157
+ "AND TABLE_NAME = :table_name "
158
+ "AND COLUMN_NAME = :column",
159
+ {"table_name": table_name, "column": column},
160
+ )
161
+ if not rows:
162
+ return None
163
+ value = rows[0]["column_type"]
164
+ if not isinstance(value, str):
165
+ return None
166
+ matched = re.fullmatch(r"vector\((\d+)\)", value.strip(), re.IGNORECASE)
167
+ return int(matched.group(1)) if matched else None
168
+
127
169
  @classmethod
128
170
  def _ensure_vector_supported(cls) -> None:
129
171
  """确保服务端支持 VECTOR 类型(需 MySQL 9.x)
@@ -27,7 +27,7 @@ from __future__ import annotations
27
27
 
28
28
  本类继承 BaseDbKit,复用全部共享实现(连接管理 / ORM / 原生 SQL / 原生表操作 /
29
29
  事务 / 表操作),并覆写 PostgreSQL 专属的 get_table_info,另提供基于 pgvector 的
30
- 4 个向量方法(签名与 SQLiteKit / MySqlKit 完全一致)。
30
+ 5 个向量方法(签名与 SQLiteKit / MySqlKit 完全一致)。
31
31
 
32
32
  使用方式::
33
33
 
@@ -108,6 +108,7 @@ class PgKit(BaseDbKit):
108
108
  "distances": SUPPORTED_DISTANCE_TYPES,
109
109
  "distance_kind": "true", # exact=True 与 False 下 distance 都是真值
110
110
  "fast_distance_kind": "true", # 近似仅影响候选集(索引),数值仍是真值
111
+ "dimension_kind": "exact", # vector_column_dimension 返回「必须相等」的精确维度
111
112
  }
112
113
 
113
114
  @classmethod
@@ -149,6 +150,48 @@ class PgKit(BaseDbKit):
149
150
 
150
151
  # ==================== 向量操作(pgvector) ====================
151
152
 
153
+ @classmethod
154
+ def vector_column_dimension(
155
+ cls, table_name: str, column: str = "embedding"
156
+ ) -> int | None:
157
+ """查询向量列的声明维度(pgvector 的 ``vector`` 类型)
158
+
159
+ Args:
160
+ table_name: 表名(**非限定名**,按 ``search_path`` 解析,与 `get_table_info` 同源)
161
+ column: 向量列名(默认 ``embedding``)
162
+
163
+ Returns:
164
+ 列声明的维度;下列情况返回 ``None``:列不存在、不是 ``vector`` 类型、
165
+ 建为**无维度**的 ``vector``
166
+
167
+ Note:
168
+ - pgvector 把维度存在 ``pg_attribute.atttypmod``,值**直接等于维度**
169
+ (实测 pgvector 0.8.7:``vector(1024)`` → 1024,无 varchar 那样的 +4 偏移)。
170
+ - 必须联 ``pg_type`` 校验类型名:``varchar(20)`` / ``numeric`` 等同样有
171
+ ``atttypmod``,不判类型会误报出维度。
172
+ - 本后端维度是**硬约束**(写入维度必须相等),故
173
+ ``VECTOR_CAPABILITIES["dimension_kind"] == "exact"``。
174
+ - 用 ``to_regclass()`` 定位(表不存在返回 NULL 而非报错);
175
+ **不要**改用 ``CAST(:表名 AS regclass)``。
176
+ """
177
+ rows = cls.query(
178
+ "SELECT a.atttypmod AS typmod "
179
+ "FROM pg_attribute a "
180
+ "JOIN pg_type t ON t.oid = a.atttypid "
181
+ "WHERE a.attrelid = to_regclass(:table_name) "
182
+ "AND a.attname = :column "
183
+ "AND a.attnum > 0 "
184
+ "AND NOT a.attisdropped "
185
+ "AND t.typname = 'vector'",
186
+ {"table_name": table_name, "column": column},
187
+ )
188
+ if not rows:
189
+ return None
190
+ typmod = rows[0]["typmod"]
191
+ if not isinstance(typmod, int) or typmod <= 0:
192
+ return None
193
+ return typmod
194
+
152
195
  @classmethod
153
196
  def _ensure_vector_extension(cls) -> None:
154
197
  """确保 pgvector 扩展可用(缺失则尝试创建)
@@ -26,7 +26,7 @@ from __future__ import annotations
26
26
  业务代码无需管理 session/transaction,直接调用即可。
27
27
 
28
28
  本类继承 BaseDbKit,复用全部共享实现(连接管理 / ORM / 原生 SQL / 原生表操作 /
29
- 事务 / 表操作),并额外提供基于 sqlite-vector 扩展的 4 个向量方法,以及 2 个
29
+ 事务 / 表操作),并额外提供基于 sqlite-vector 扩展的 5 个向量方法,以及 2 个
30
30
  **编码原语**(`encode_vector` / `decode_vector`,仅本后端具备)。
31
31
 
32
32
  使用方式::
@@ -98,6 +98,7 @@ class SQLiteKit(BaseDbKit):
98
98
  "distances": SUPPORTED_DISTANCE_TYPES, # 支持的距离类型
99
99
  "distance_kind": "true", # exact=True 时的 distance 口径
100
100
  "fast_distance_kind": "quantized_l2_x255", # exact=False 时的 distance 口径
101
+ "dimension_kind": None, # 无维度元数据,vector_column_dimension 恒返回 None
101
102
  }
102
103
 
103
104
  @classmethod
@@ -176,6 +177,33 @@ class SQLiteKit(BaseDbKit):
176
177
 
177
178
  return [float(x) for x in np.frombuffer(blob, dtype=_VECTOR_DTYPE)]
178
179
 
180
+ @classmethod
181
+ def vector_column_dimension(
182
+ cls, table_name: str, column: str = "embedding"
183
+ ) -> int | None:
184
+ """查询向量列的声明维度,**本后端恒返回 None**
185
+
186
+ Args:
187
+ table_name: 表名(未使用,仅为保持三库签名一致)
188
+ column: 向量列名(未使用,同上)
189
+
190
+ Returns:
191
+ 恒为 ``None``
192
+
193
+ Note:
194
+ SQLite 没有向量类型,也没有维度元数据:`create_vector_table` 把维度落实为
195
+ ``CHECK (length(column) = dims * 4)``,写入时由数据库强制,但**查不出来**。
196
+
197
+ 不解析 ``sqlite_master.sql`` 里的 CHECK 文本:列名转义、既有表无该约束、
198
+ 文本格式都可能变形,反解不可靠。
199
+
200
+ 守卫建议:维度约束已由 ``CHECK`` 承担(写入维度不符即报错),无需前置探测;
201
+ 若确需提前校验,可读一行数据的 ``length(column) / 4`` 推断(空表无法推断)。
202
+
203
+ ``VECTOR_CAPABILITIES["dimension_kind"] is None`` 表明本后端不支持该查询。
204
+ """
205
+ return None
206
+
179
207
  @classmethod
180
208
  def create_vector_table(
181
209
  cls,
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