Metadata-Version: 2.5
Name: sdpy-kit
Version: 0.1.14
Summary: SDPY Kit——统一数据库访问层（PG/MySQL/SQLite）与 AI/MCP/Redis/文件/文档/向量工具集
Author-email: Clark <changhongyuan@126.com>
License-Expression: MIT
Keywords: ai,kit,mcp,mysql,postgresql,redis,sdpy,sqlalchemy,sqlite,sqlmodel,向量检索,工具集,开发工具,数据库
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.13
Requires-Dist: pydantic-settings>=2.15.0
Requires-Dist: pydantic>=2.13.5
Requires-Dist: sqlalchemy~=2.0.54
Requires-Dist: sqlmodel>=0.0.46
Provides-Extra: ai
Requires-Dist: httpx>=0.28.1; extra == 'ai'
Requires-Dist: python-dotenv>=1.2.3; extra == 'ai'
Provides-Extra: all
Requires-Dist: docxtpl>=0.20.2; extra == 'all'
Requires-Dist: fastmcp>=4.0.5; extra == 'all'
Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'all'
Requires-Dist: httpx>=0.28.1; extra == 'all'
Requires-Dist: markdown-it-py>=4.2.0; extra == 'all'
Requires-Dist: numpy>=2.5.3; extra == 'all'
Requires-Dist: pikepdf>=10.13.0; extra == 'all'
Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'all'
Requires-Dist: psycopg2-binary>=2.9.13; extra == 'all'
Requires-Dist: pymysql>=1.2.3; extra == 'all'
Requires-Dist: python-dotenv>=1.2.3; extra == 'all'
Requires-Dist: redis>=8.1.0; extra == 'all'
Requires-Dist: requests>=2.34.2; extra == 'all'
Requires-Dist: sqliteai-vector>=1.1.2; extra == 'all'
Requires-Dist: zvec>=0.7.0; extra == 'all'
Provides-Extra: anydoc
Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'anydoc'
Requires-Dist: pikepdf>=10.13.0; extra == 'anydoc'
Provides-Extra: docx
Requires-Dist: docxtpl>=0.20.2; extra == 'docx'
Provides-Extra: mcp
Requires-Dist: fastmcp>=4.0.5; extra == 'mcp'
Provides-Extra: md2doc
Requires-Dist: markdown-it-py>=4.2.0; extra == 'md2doc'
Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'md2doc'
Provides-Extra: mineru
Requires-Dist: requests>=2.34.2; extra == 'mineru'
Provides-Extra: mysql
Requires-Dist: pymysql>=1.2.3; extra == 'mysql'
Provides-Extra: pg
Requires-Dist: psycopg2-binary>=2.9.13; extra == 'pg'
Provides-Extra: redis
Requires-Dist: redis>=8.1.0; extra == 'redis'
Provides-Extra: sqliteai
Requires-Dist: numpy>=2.5.3; extra == 'sqliteai'
Requires-Dist: sqliteai-vector>=1.1.2; extra == 'sqliteai'
Provides-Extra: zvec
Requires-Dist: zvec>=0.7.0; extra == 'zvec'
Description-Content-Type: text/markdown

# sdpy-kit

SDPY 统一工具集（Kit 体系），供 Python 项目开箱即用：

- **统一数据库访问层**：PostgreSQL / MySQL / SQLite，三个 Kit API 完全一致，切换数据库只改一行 import；
- **AI 多模型服务池**：chat / embedding / rerank / image 四类能力，多厂商候选链 + 故障自动转移；
- **MCP 客户端**：streamable-http / sse 双传输，档案驱动多服务器；
- **工具集**：Redis / 文件 / 雪花 ID / 文档互转 / Word 模板填充 / MinerU 解析 / 向量库 / 微信公众号文章抓取存储。

> 要求 Python 3.13+

## 模块与方法入口

| 模块 | 类 / 入口 | 能力 | 需要 extra |
|------|-----------|------|-----------|
| `kit.config` | `get_settings` / `get_default_logger` | 配置单例与统一日志 | 无 |
| `kit.db` | `SQLiteKit` / `PgKit` / `MySqlKit` / `CRUDBase` | 统一 CRUD、原生 SQL、事务、表管理 | `pg` / `mysql` / `sqliteai` |
| `kit.ai` | `AI`（同步门面）/ 4 个原生异步 Service | 对话 / 向量化 / 重排序 / 文生图 | `ai` |
| `kit.mcp_kit` | `McpKit` | MCP 工具发现与调用 | `mcp` |
| `kit.redis_kit` | `RedisKit` | Redis string / hash / list / set / zset | `redis` |
| `kit.file_kit` | `FileKit` | 文件与目录读写、移动、遍历 | 无 |
| `kit.common_kit` | `CommonKit` | 雪花 ID、时间戳 ID、时间格式化 | 无 |
| `kit.anydoc_kit` | `AnyDocKit` | docx / pdf / xlsx 等 → Markdown | `anydoc` |
| `kit.docx_kit` | `DocxKit` | Word 模板动态填充（章节 / 列表 / 表格） | `docx` |
| `kit.md2doc_kit` | `Md2DocKit` | Markdown → Word / PDF / HTML | `md2doc` |
| `kit.mineru_kit` | `MinerUKit` | MinerU 文档解析（精准 + Agent 轻量） | `mineru` |
| `kit.zvec_kit` | `ZvecKit` | zvec 本地向量库 | `zvec` |
| `kit.wx_article_kit` | `WxArticleKit` | 微信公众号文章 → Markdown + 图片转存 | 无 |

## 安装

```bash
pip install sdpy-kit            # 基础版：config/log + kit.db 核心（SQLite 常规操作零额外依赖）
# 或
uv add sdpy-kit
```

可选能力按 extras 按需安装（均为延迟导入，不装不影响基础使用，缺依赖时报错会提示安装命令）：

| extra | 能力 | 安装 |
|-------|------|------|
| `pg` | PgKit（PostgreSQL 驱动） | `pip install "sdpy-kit[pg]"` |
| `mysql` | MySqlKit（MySQL 驱动） | `pip install "sdpy-kit[mysql]"` |
| `sqliteai` | SQLiteKit 向量方法（numpy / sqliteai-vector，SQLite 向量扩展） | `pip install "sdpy-kit[sqliteai]"` |
| `zvec` | ZvecKit（zvec，阿里独立向量库） | `pip install "sdpy-kit[zvec]"` |
| `redis` | RedisKit | `pip install "sdpy-kit[redis]"` |
| `mcp` | McpKit（fastmcp） | `pip install "sdpy-kit[mcp]"` |
| `ai` | AI 服务池（httpx + python-dotenv） | `pip install "sdpy-kit[ai]"` |
| `md2doc` | Md2DocKit：Markdown → Word / PDF / HTML（pimd / fpdf2） | `pip install "sdpy-kit[md2doc]"` |
| `anydoc` | AnyDocKit 文档转 Markdown（firecrawl-anydoc + pikepdf） | `pip install "sdpy-kit[anydoc]"` |
| `docx` | DocxKit 模板动态填充（docxtpl） | `pip install "sdpy-kit[docx]"` |
| `mineru` | MinerUKit 解析 API（精准解析 + Agent 轻量解析） | `pip install "sdpy-kit[mineru]"` |
| `all` | 全部能力 | `pip install "sdpy-kit[all]"` |

## 配置（.env）

kit 通过 pydantic-settings 读取项目根目录 `.env`；**同名真实环境变量优先于 `.env`**。全部变量都有默认值，按需覆盖即可。

### 最小样例 `.env`

```dotenv
# 日志（可选）
LOG_LEVEL=INFO
LOG_TO_FILE=false

# 数据库（三选一，切换只改 DEFAULT_DB_TYPE：postgresql / mysql / sqlite）
DEFAULT_DB_TYPE=sqlite
SQLITE_DB_PATH=./data/sqlite.db
# DB_HOST=localhost
# DB_PORT=5432
# DB_DATABASE=postgres
# DB_USER=admin
# DB_PASSWORD=
# MYSQL_PORT=3306
# MYSQL_DATABASE=mydb
# MYSQL_USER=root
# MYSQL_CHARSET=utf8mb4

# 按需启用以下能力
# REDIS_HOST=localhost
# REDIS_PORT=6379
# REDIS_DB=0
# REDIS_PASSWORD=
# FILE_STORAGE_ROOT=./data
# ZVEC_DATA_DIR=./data/zvec

# AI：档案路径 + 你为各模型档案自定的密钥变量名（变量名在 ai_models.json 里声明）
# AI_MODELS_CONFIG_PATH=./config/ai_models.json
# SILICONFLOW_API_KEY=sk-xxxxxx

# MCP：档案路径 + 你为各服务器自定的 token 变量名（变量名在 mcp_servers.json 里声明）
# MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.json
# AMINER_TOKEN=xxxxxx

# MinerU（仅精准解析需要 Key，Agent 轻量解析免登录）
# MINERU_API_KEY=
```

### 环境变量 ↔ 能力对照表

> 表中未出现的模块（`kit.common_kit`、`kit.docx_kit`、`kit.md2doc_kit`、`kit.wx_article_kit`）不读取任何环境变量。

**基础与日志**（影响 `kit.config` 及所有模块的日志输出）

| 环境变量 | 默认值 | 作用 |
|----------|--------|------|
| `LOG_LEVEL` | `INFO` | kit 统一日志级别 |
| `LOG_TO_CONSOLE` | `true` | 是否输出到控制台 |
| `LOG_TO_FILE` | `false` | 是否输出到文件 |
| `LOG_DIR` | `./logs` | 日志目录 |
| `LOG_FILE` | `app.log` | 日志文件名 |

**数据库** → `kit.db`、`SQLiteKit` / `PgKit` / `MySqlKit`

| 环境变量 | 默认值 | 作用 |
|----------|--------|------|
| `DEFAULT_DB_TYPE` | `postgresql` | 默认数据库类型：`postgresql` / `mysql` / `sqlite`；切换数据库只改这一项 |
| `DB_HOST` | `localhost` | PG / MySQL 共用主机 |
| `DB_PORT` | `5432` | PostgreSQL 端口 |
| `DB_DATABASE` | `postgres` | PostgreSQL 库名 |
| `DB_USER` | `admin` | PostgreSQL 用户 |
| `DB_PASSWORD` | `1qaz@WSX3edc` | PG / MySQL 共用密码 |
| `MYSQL_PORT` | `3306` | MySQL 端口（与 PG 不同时覆盖） |
| `MYSQL_DATABASE` | `mydb` | MySQL 库名 |
| `MYSQL_USER` | `root` | MySQL 用户 |
| `MYSQL_CHARSET` | `utf8mb4` | MySQL 字符集 |
| `SQLITE_DB_PATH` | `./data/sqlite.db` | SQLite 文件路径（`:memory:` 时自动共享单连接） |

**Redis** → `kit.redis_kit`

| 环境变量 | 默认值 | 作用 |
|----------|--------|------|
| `REDIS_HOST` | `localhost` | Redis 主机 |
| `REDIS_PORT` | `6379` | Redis 端口 |
| `REDIS_DB` | `0` | 数据库编号 |
| `REDIS_PASSWORD` | `None` | 密码，无密码留空 |

**文件存储** → `kit.file_kit`

| 环境变量 | 默认值 | 作用 |
|----------|--------|------|
| `FILE_STORAGE_ROOT` | `./data` | FileKit 全部相对路径的根目录。⚠ 该目录**必须已存在**，否则首次调用抛 `RuntimeError`（根路径不存在） |

**向量库** → `kit.zvec_kit`

| 环境变量 | 默认值 | 作用 |
|----------|--------|------|
| `ZVEC_DATA_DIR` | `./data/zvec` | Collection 根目录 |
| `ZVEC_DEFAULT_METRIC` | `cosine` | 默认距离度量（`cosine` / `l2` / `ip`） |
| `ZVEC_QUERY_THREADS` | `None`（自适应） | 查询线程数 |
| `ZVEC_OPTIMIZE_THREADS` | `None`（自适应） | 索引构建线程数 |
| `ZVEC_MEMORY_LIMIT_MB` | `None`（自适应） | 内存软上限 |
| `ZVEC_LOG_DIR` | `./logs/zvec` | zvec 引擎独立日志目录 |
| `ZVEC_LOG_LEVEL` | `warn` | zvec 引擎日志级别 |

**AI** → `kit.ai`

| 环境变量 | 默认值 | 作用 |
|----------|--------|------|
| `AI_MODELS_CONFIG_PATH` | `./config/ai_models.json` | 档案文件位置；文件不存在时调用抛 `AIConfigError` |
| `KIT_AI_CHAIN_TIMEOUT` | 未设置 | 全局整链预算（秒）= 门面同步等待上限。⚠ **不是 `Settings` 字段**，由代码直读 `os.environ`，无类型/范围校验，非法值仅告警后回退 |
| *（密钥，变量名由档案决定）* | 无 | 名字写在 `ai_models.json` 各档案的 `api_key` 字段（如 `SILICONFLOW_API_KEY`），值放 `.env` 或系统环境变量。未配置的档案会被剔除出候选链；该类型全部未配置则调用失败 |

**MCP** → `kit.mcp_kit`

| 环境变量 | 默认值 | 作用 |
|----------|--------|------|
| `MCP_SERVERS_CONFIG_PATH` | `./config/mcp_servers.json` | 服务器档案位置 |
| `MCP_CALL_TIMEOUT` | `60`（1–600） | 档案未声明 `timeout` 时的调用超时 |
| *（令牌，变量名由档案决定）* | 无 | 名字写在 `mcp_servers.json` 各服务器的 `token_env` 字段（如 `AMINER_TOKEN`）；档案声明了却两处都未配置时抛 `McpKitError` |

**MinerU** → `kit.mineru_kit`

| 环境变量 | 默认值 | 作用 |
|----------|--------|------|
| `MINERU_API_KEY` | 无 | 精准解析 Token（Agent 轻量解析免登录，不读该变量）；缺失时抛 `MinerUError` |
| `MINERU_BASE_API_URL` | `https://mineru.net` | API 基础地址 |
| `MINERU_TIMEOUT` | `60`（1–600） | 单次 HTTP 请求连接超时 |
| `MINERU_TRANSFER_TIMEOUT` | `300`（1–3600） | 文件上传 / 结果下载读取超时 |
| `MINERU_POLL_INTERVAL` | `2.0`（≥0.5） | 任务轮询间隔 |
| `MINERU_MAX_WAIT` | `600`（≥10） | 轮询总超时，超时抛 `MinerUError` |

**云 OCR**（任何 `ocr="hosted"` 的转换）→ `kit.anydoc_kit`

| 环境变量 | 默认值 | 作用 |
|----------|--------|------|
| `FIRECRAWL_API_KEY` | 无 | 扫描件云 OCR 密钥。kit **不读**该变量：仅在方法参数 `api_key=None` 时由 firecrawl-anydoc 自行回落读取 |
| `FIRECRAWL_API_URL` | 无 | 云 OCR 服务地址，回落规则同上 |

### 环境变量的四类来源

| 类别 | 命名方式 | 当前变量 | 读取方与语义 |
|------|----------|----------|--------------|
| ① Settings 变量 | 固定，kit 定义 | 上表中除 `KIT_AI_CHAIN_TIMEOUT` 与密钥类之外的全部 | pydantic-settings 读 `.env` 与真实环境变量（后者优先），带类型与范围校验，缺失用默认值 |
| ② 直读变量 | 固定，kit 代码直接读 `os.environ` | `KIT_AI_CHAIN_TIMEOUT` | 无类型校验，非法值告警后回退 |
| ③ 档案声明变量 | **动态**，名字写在 JSON 里 | AI 的 `api_key`、MCP 的 `token_env` | **名字进 JSON、值进 `.env`**——所以档案文件可以安全入库，密钥留在环境里 |
| ④ 第三方库变量 | 由依赖库定义 | `FIRECRAWL_API_KEY`、`FIRECRAWL_API_URL` | kit 只透传 `None`，由 anydoc 自己回落读取 |

### 密钥解析优先级

| 能力 | 解析顺序 | 全部缺失时 |
|------|----------|------------|
| AI | 真实环境变量 > 项目根 `.env`（首次加载档案时经 `load_dotenv(override=False)` 注入 `os.environ`） | 该档案被剔除出候选链；该类型无可用档案则调用报 `AIAggregateError` |
| MCP | `os.environ` > 项目根 `.env`（kit 自行解析文件） | 抛 `McpKitError` |
| MinerU | 仅 `Settings`（`.env` / 真实环境变量） | 精准解析接口抛 `MinerUError` |

> AI 的 `load_dotenv(override=False)` 表示已存在的真实环境变量不会被 `.env` 覆盖——生产用容器 / 系统环境变量注入时，可以放心保留开发用的 `.env`。

### 改了配置何时生效

| 改动内容 | 生效时机 |
|----------|----------|
| `LOG_*`、`DB_*`、`MYSQL_*`、`REDIS_*`、`FILE_STORAGE_ROOT`、`ZVEC_*`、`MINERU_*` | `get_settings()` 带 `lru_cache`，进程内首次读取后固定 → **重启进程**（或调用 `AI.reload_config()`，它会顺带清空该缓存） |
| AI 密钥写在 `.env` 里 | `.env` 只在加载档案时注入一次 → 重启，或 `AI.reload_config(reload_file=True)` |
| AI 密钥由自己写入 `os.environ` | `AI.reload_config()`（默认 `reload_file=False`，只重读 `os.environ`） |
| `ai_models.json`（增删档案、换 model） | `AI.reload_config(reload_file=True)` |
| `KIT_AI_CHAIN_TIMEOUT` | 每次调用现读 `os.environ` → 改 `os.environ` 立即生效；写在 `.env` 里的新值要等下一次档案加载 |
| MCP token | 每次调用现读 `os.environ` 并回退解析 `.env` → **改 `.env` 立即生效，无需重启** |

---

## kit.config —— 配置与日志

```python
from kit.config import get_settings, get_default_logger

settings = get_settings()               # Settings 单例（lru_cache，读取 .env）
logger = get_default_logger(__name__)   # 统一日志入口（禁止自建 logging handler）
logger.info("hello %s", settings.FILE_STORAGE_ROOT)
```

| 入口 | 说明 |
|------|------|
| `get_settings() -> Settings` | 返回全局配置单例，字段名与环境变量同名（如 `settings.SQLITE_DB_PATH`、`settings.DEFAULT_DB_TYPE`），完整清单见上文对照表 |
| `get_default_logger(name=None, log_level=None, log_file=None) -> logging.Logger` | 获取统一格式日志器（控制台 + 轮转文件） |
| `setup_logger(name=None, log_level=None, log_file=None, log_format=..., date_format=..., max_bytes=5*1024*1024, backup_count=5) -> logging.Logger` | 完整参数版本，一般无需直接使用 |

---

## kit.db —— 统一数据库访问层

`kit/__init__.py` 直接重导出 `kit.db` 的全部公开符号，下列两种写法等价：

```python
from kit.db import session_scope, transactional, DBError    # 推荐
from kit import session_scope, transactional, DBError       # 等价
```

### 1. 三个数据库 Kit（API 完全一致）

| 类 | 导入 | 数据库 | extra |
|----|------|--------|-------|
| `SQLiteKit` | `from kit.db.sqlite_kit import SQLiteKit` | SQLite | 无（常规操作） |
| `PgKit` | `from kit.db.pg_kit import PgKit` | PostgreSQL | `pg` |
| `MySqlKit` | `from kit.db.mysql_kit import MySqlKit` | MySQL | `mysql` |

**共有方法（19 个，全部为 classmethod，内部自动提交）**

| 分组 | 方法 | 说明 |
|------|------|------|
| 连接 | `connect() -> bool` | 建引擎并验证连通 |
| 连接 | `close() -> None` | 关闭引擎 |
| 连接 | `is_connected() -> bool` | 探测当前是否可连 |
| ORM | `create(model, data) -> Model` | 插入单条 |
| ORM | `create_multi(model, data_list) -> list[Model]` | 批量插入 |
| ORM | `get(model, id) -> Model \| None` | 按主键查询 |
| ORM | `get_by(model, field_name, field_value) -> Model \| None` | 按字段等值查询 |
| ORM | `list(model, *, skip=0, limit=100, order_by=None, desc=False) -> list[Model]` | 分页 + 排序查询 |
| ORM | `update(model, id, data) -> Model` | 按主键更新 |
| ORM | `delete(model, id) -> bool` | 按主键删除 |
| ORM | `upsert(model, data, conflict_fields) -> Model` | 原子插入或更新（`conflict_fields` 须为唯一约束） |
| ORM | `count(model) -> int` | 记录总数 |
| 原生 SQL | `query(sql, params=None) -> list[dict]` | 原生查询，返回字典列表 |
| 原生 SQL | `execute(sql, params=None) -> int` | 原生写入，返回影响行数 |
| 事务 | `transaction()` | 事务上下文管理器，`with PgKit.transaction() as session:` |
| 表 | `list_tables() -> list[str]` | 列出全部表名 |
| 表 | `table_exists(table_name) -> bool` | 判断表是否存在 |
| 表 | `create_tables(models) -> None` | 按模型列表建表 |
| 表 | `get_table_info(table_name) -> list[dict]` | 获取列信息（三个 Kit 各自实现） |

```python
from sqlmodel import SQLModel, Field
from kit.db.sqlite_kit import SQLiteKit

class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str

SQLiteKit.connect()
SQLiteKit.create_tables([User])

user = SQLiteKit.create(User, {"name": "张三"})           # 自动提交
SQLiteKit.update(User, user.id, {"name": "李四"})
users = SQLiteKit.list(User, skip=0, limit=10, order_by="id", desc=True)

rows = SQLiteKit.query("SELECT * FROM user WHERE name = :name", {"name": "李四"})
affected = SQLiteKit.execute("UPDATE user SET name = '王五' WHERE id = :id", {"id": user.id})

with SQLiteKit.transaction() as session:                  # 多操作原子提交
    SQLiteKit.create(User, {"name": "批量一"})
    SQLiteKit.create(User, {"name": "批量二"})
```

> 切换数据库：把 import 的 `SQLiteKit` 换成 `PgKit` / `MySqlKit` 即可，其余代码不变。

### 2. SQLiteKit 独有方法

无需额外依赖：

| 方法 | 说明 |
|------|------|
| `raw_connection()` | 取原生 DBAPI 连接（sqlite-vector 扩展已自动加载），调用方负责关闭 |
| `create_raw_table(table_name, columns, if_not_exists=True) -> None` | 按「列名 → 列定义」建普通表 |
| `raw_insert(table_name, data) -> int` | 插入一行，返回影响行数 |
| `raw_get_by(table_name, column, value) -> dict \| None` | 按单列等值取首行 |

向量方法（需 `pip install "sdpy-kit[sqliteai]"`）：

| 方法 | 说明 |
|------|------|
| `create_vector_table(table_name, id_col="id", dims=384, extra_cols=None) -> None` | 建含向量列的表 |
| `insert_vectors(table_name, vectors, ids=None, extra_data=None) -> int` | 批量插入向量 |
| `search_vectors(table_name, query_vector, top_k=10, distance_type="COSINE", filter_sql=None) -> list[dict]` | KNN 检索，按距离升序返回 `{"id", "distance", ...}` |
| `vector_version() -> str` | 向量扩展版本，不可用时返回 `"unavailable"` |

```python
SQLiteKit.create_vector_table("images", dims=384)
SQLiteKit.insert_vectors("images", [[0.1] * 384, [0.2] * 384])
rows = SQLiteKit.search_vectors("images", [0.1] * 384, top_k=5)
```

### 3. CRUDBase（配合 session_scope，不自动提交）

```python
from kit.db import CRUDBase, session_scope

user_crud = CRUDBase(User)

with session_scope() as session:          # 正常退出提交、异常回滚
    user_crud.create(session, {"name": "张三"})
    user_crud.update(session, user_crud.get(session, 1), {"name": "李四"})
```

| 方法 | 说明 |
|------|------|
| `CRUDBase(model)` | 构造，传入 SQLModel 模型类 |
| `get(session, id) -> Model \| None` | 按主键取单条 |
| `get_or_raise(session, id) -> Model` | 按主键取，缺失抛 `NotFoundError` |
| `get_multi(session, *, skip=0, limit=100, order_by=None, desc=False) -> list[Model]` | 分页 + 排序 |
| `get_by_field(session, field_name, field_value) -> Model \| None` | 按字段等值取单条 |
| `get_multi_by_field(session, field_name, field_value, *, skip=0, limit=100) -> list[Model]` | 按字段等值分页取多条 |
| `create(session, obj_in) -> Model` | 插入单条（add + flush） |
| `create_multi(session, objs_in) -> list[Model]` | 批量插入（add_all + flush） |
| `update(session, db_obj, obj_in) -> Model` | 更新，入参为模型实例或字典 |
| `update_multi(session, ids, obj_in) -> int` | 单条批量 UPDATE，返回影响行数 |
| `delete(session, id) -> Model \| None` | 按主键删除 |
| `delete_multi(session, ids) -> int` | 单条批量 DELETE，返回影响行数 |
| `count(session) -> int` | 记录总数 |
| `exists(session, id) -> bool` | 主键是否存在 |
| `upsert(session, obj_in, conflict_fields) -> Model` | 原生 `INSERT ... ON CONFLICT / ON DUPLICATE KEY` 原子语句 |

> 约定：`SQLiteKit / PgKit / MySqlKit` 方法自动提交；`CRUDBase` 方法**不提交**，事务边界由外层 `session_scope` 或 `@transactional` 决定。

### 4. 引擎 / 会话 / 事务 / 异常

| 符号 | 签名 | 说明 |
|------|------|------|
| `get_engine` | `(db_type=None, **kwargs) -> Engine` | 线程安全引擎单例，`**kwargs` 首次创建时透传 `create_engine` |
| `create_tables` | `(models, db_type=None) -> None` | 按模型列表建表 |
| `list_tables` | `(db_type=None) -> list[str]` | 列出全部表 |
| `table_exists` | `(table_name, db_type=None) -> bool` | 判断表是否存在 |
| `close_engine` | `(db_type) -> None` | 关闭并移除指定类型引擎 |
| `close_all_engines` | `() -> None` | 关闭全部引擎 |
| `session_scope` | `(db_type=None, *, autocommit=True)` | 唯一会话入口：正常提交、异常回滚、最终关闭 |
| `get_session` | `(db_type=None) -> Session` | 裸 Session，调用方自管生命周期 |
| `transactional` | `(db_type=None, *, retry_count=0, retry_delay=0.5)` | 事务装饰器：自动注入 `session` 参数、失败退避重试 |
| `POSTGRESQL` / `MYSQL` / `SQLITE` | `"postgresql"` / `"mysql"` / `"sqlite"` | 数据库类型常量 |
| `DBError` | 继承 `SQLAlchemyError` | 数据库异常基类 |
| `NotFoundError` | 继承 `DBError` | 记录不存在 |

```python
from kit.db import transactional
from sqlmodel import Session

@transactional(retry_count=3)
def transfer(session: Session, from_id: int, to_id: int, amount: float) -> None:
    # 被装饰函数必须接受 session 参数；退出时自动提交，异常自动回滚并重试
    ...
```

### 5. 模型混入类与查询构建器

```python
from kit.db.models import TimestampMixin, SoftDeleteMixin, AuditMixin

class Article(TimestampMixin, SoftDeleteMixin, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
```

| 混入类 | 字段 |
|--------|------|
| `TimestampMixin` | `created_at`（UTC）、`updated_at` |
| `SoftDeleteMixin` | `is_deleted`、`deleted_at` |
| `AuditMixin` | 时间戳字段 + `created_by`、`updated_by` |

查询构建器需显式从子模块导入（未进入 `kit.db` 顶层导出）：

```python
from kit.db.query import QueryBuilder, QueryCache, QueryOptimizer, cached_query

qb = QueryBuilder(User, session)
active = qb.filter(User.name == "张三").all()      # 链式不可变，每步返回新实例
page = qb.paginate(page=1, per_page=20)            # {"items", "total", ...}
```

| 类 | 方法 |
|----|------|
| `QueryBuilder(model, session)` | `filter(*conditions)`、`filter_by(**kwargs)`、`join(target, onclause=None, isouter=False)`、`order_by(*columns)`、`limit(n)`、`offset(n)`、`distinct()`、`only(*field_names)`、`all()`、`first()`、`count()`、`exists()`、`paginate(page=1, per_page=20)` |
| `QueryCache(default_ttl=300, maxsize=1000)` | `get(key)`、`set(key, value, ttl=None)`、`delete(key)`、`clear()`、`clean_expired()` |
| `QueryOptimizer(session)` | `explain(statement)`、`suggest_indexes(model, query_fields)`、`measure(statement, params=None)`、`stats`、`clear_stats()` |
| `cached_query(ttl=300, maxsize=1000)` | 查询结果缓存装饰器 |

---

## kit.ai —— AI 多模型服务池

安装：`pip install "sdpy-kit[ai]"`
档案：项目根 `config/ai_models.json`（见文末「AI / MCP 档案文件」）

### 1. 同步门面 `AI`（推荐）

```python
from kit.ai.runner import AI, shutdown

text = AI.chat_complete("用一句话介绍 Python")                 # 单轮对话
hits = AI.rerank("水声通信", ["文档A", "文档B"], top_n=2)      # 重排序
url = AI.generate_image("一只在写代码的猫")                    # 文生图
AI.use("chat", "glm")                                          # 切换当前档案（进程内长期生效）
print(AI.available("chat"))                                    # 查看各档案状态快照
shutdown()                                                     # 应用退出前释放资源（幂等）
```

| 分组 | 方法 | 说明 |
|------|------|------|
| chat | `chat_complete(prompt, *, max_tokens=None) -> str` | 单轮对话（仅 user 消息） |
| chat | `chat_ask(messages, *, max_tokens=None) -> str` | 多轮对话，`messages` 为 `[{"role": "user", "content": "..."}]` |
| chat | `chat_stream(messages, *, max_tokens=None) -> Iterator[str]` | 同步流式逐 token 迭代器（建议 `with` 使用） |
| embedding | `embed_text(text) -> list[float]` | 单文本向量化 |
| embedding | `embed_texts(texts) -> list[list[float]]` | 批量向量化 |
| rerank | `rerank(query, documents, top_n=None) -> list[tuple[int, float]]` | 返回 `[(原索引, 相关分)]` 降序列表 |
| image | `generate_image(prompt, *, size=None, n=1) -> str` | 文生图，返回图片 URL |
| 管理 | `use(model_type, profile) -> None` | 切换该类型当前档案（`chat` / `embedding` / `rerank` / `image`） |
| 管理 | `available(model_type) -> list[ProfileStatus]` | 各档案状态快照（含未配置与被剔除者） |
| 管理 | `reload_config(*, model_type=None, reload_file=False, probe=True, immediate=False) -> ReloadResult` | 配置热重载，改密钥 / 档案后无需重启 |

```python
from kit.ai.runner import AI

with AI.chat_stream([{"role": "user", "content": "写一首短诗"}]) as stream:
    for token in stream:      # 逐 token 消费
        print(token, end="", flush=True)
```

> 门面仅面向**同步上下文**；若调用点本身在 async 函数内，请直接 `await` 原生异步服务，避免阻塞事件循环线程。
> 模块级另有 `kit.ai.runner.reload_config(...)`、`run_sync(coro_factory, *, timeout=120.0)`、`shutdown()`。

### 2. 原生异步服务

```python
from kit.ai import (
    ChatService, EmbeddingService, RerankService, ImageService,   # 服务类
    ChatRequest, EmbeddingRequest, RerankRequest, ImageRequest,   # 请求模型
    RerankHit, ModelBuilder,                                      # 结果类型 / 构建器
)

service = ModelBuilder.build_chat()                 # 老按类型构建：build_chat / build_embedding / build_rerank / build_image
answer = await service.ask(messages)                # 或 await service.complete(prompt)
await service.close()
```

| 类 | 方法 |
|----|------|
| `ChatService` | `ask(messages, *, temperature=None, max_tokens=None, deadline=None) -> str`、`complete(prompt, *, ...) -> str`、`stream(messages, *, ...) -> AsyncIterator[str]` |
| `EmbeddingService` | `embed_text(text) -> list[float]`、`embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]`、`dimension` 属性 |
| `RerankService` | `rerank(query, documents, *, top_n=None) -> list[RerankHit]`（已按 score 降序） |
| `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> str` |
| `ModelBuilder` | `build(model_type, model=None, *, api_key=None)` 及各类型专用 `build_chat / build_embedding / build_rerank / build_image` |
| `BaseAIService` | `profile` 属性、`await close()`、支持 `async with` |

### 3. 异常

```python
from kit.ai.exceptions import (
    AIError, AIConfigError, AIRequestError, AITimeoutError,
    AIAggregateError, AIResponseError,
)
```

| 异常 | 触发场景 / 可用属性 |
|------|---------------------|
| `AIError` | 基类 |
| `AIConfigError` | 档案缺失或非法、档案不存在、密钥未配置、必填字段缺失 |
| `AIRequestError` | 单次请求失败；`status_code`、`retry_after`（429 时解析 `Retry-After`） |
| `AITimeoutError` | 请求超时（继承 `AIRequestError`） |
| `AIAggregateError` | 全部候选失败或整链预算耗尽；`tried`、`statuses`、`last_error`、`budget_exhausted` |
| `AIResponseError` | 响应结构解析失败、流式数据无法解析 |

---

## kit.mcp_kit —— 通用 MCP 客户端

安装：`pip install "sdpy-kit[mcp]"`
档案：项目根 `config/mcp_servers.json`；token 值放 `.env`（档案里只写环境变量名）

```python
from kit.mcp_kit import McpKit

tools = McpKit.list_tools("aminer")                                  # [{name, description}]
result = McpKit.call_tool("search_paper_by_title", {"title": "水声通信", "size": 10})
print(result.data, result.elapsed_ms)

# 直连临时服务器（不经档案）
r = McpKit.call_tool_url("https://example.com/mcp", "query", {}, token="...")

# 异步环境 / 一次握手复用多次调用
async with McpKit.session("aminer") as call:
    r1 = await call("search_paper_by_title", {"title": "A"})
    r2 = await call("get_papers_info", {"id": "1"})
```

| 方法 | 说明 |
|------|------|
| `list_tools(server=None) -> list[dict]` | 工具发现；`server=None` 用档案 `default_server` |
| `call_tool(name, arguments, *, server=None) -> McpToolResult` | 同步短连接调用（主路径） |
| `call_tool_async(name, arguments, *, server=None) -> McpToolResult` | 异步版；FastAPI 等已有事件循环的进程内必须用 |
| `session(server=None)` | `async with` 长连接复用，产出 `call(name, arguments)` 协程函数 |
| `call_tool_url(url, name, arguments, *, token=None, transport="streamable-http", timeout=None) -> McpToolResult` | 直连临时服务器，不经档案 |

| 类型 | 说明 |
|------|------|
| `McpToolResult(server, tool, elapsed_ms, data)` | 调用结果；`data` 为 JSON 解析后对象，非 JSON 文本降级为字符串 |
| `McpKitError(message, *, detail=None)` | 业务失败时 `detail` 为服务端原始响应；连接失败时为最后一次传输层错误 |

> 传输方式仅 `streamable-http`（默认）与 `sse`；档案配 `fallback_url` 时主端点传输层失败会自动回退（业务失败不回退）。

---

## kit.redis_kit —— Redis 工具

安装：`pip install "sdpy-kit[redis]"`

```python
from kit.redis_kit import RedisKit

RedisKit.set("user:1", "clark", expire=60)
RedisKit.get("user:1")                       # -> "clark"
RedisKit.zadd("rank", {"alice": 1.0, "bob": 2.0})
RedisKit.zrange("rank", 0, -1, withscores=True)
RedisKit.close()
```

| 分组 | 方法 |
|------|------|
| 通用 | `get_client()`（上下文管理器）、`set(key, value, expire=None) -> bool`、`get(key) -> RedisValue \| None`、`delete(key) -> bool`、`exists(key) -> bool`、`keys(pattern) -> list[str]`、`close() -> None` |
| 批量 | `batch_set(key_value_pairs, expire=None) -> bool`、`batch_get(keys) -> dict`、`batch_delete(keys) -> bool` |
| Hash | `hset(key, field, value) -> bool`、`hget(key, field)`、`hgetall(key) -> dict`、`hdel(key, *fields) -> bool` |
| List | `lpush(key, *values) -> bool`、`rpush(key, *values) -> bool`、`lpop(key)`、`rpop(key)`、`lrange(key, start, stop) -> list` |
| Set | `sadd(key, *members) -> bool`、`srem(key, *members) -> bool`、`smembers(key) -> list`、`sismember(key, member) -> bool` |
| Sorted Set | `zadd(key, mapping) -> bool`、`zrange(key, start, stop, withscores=False)` |

> `RedisValue = str | int | float | bytes`；连接池 `decode_responses=True`，内置指数退避重试。

---

## kit.file_kit —— 文件工具

相对路径均相对于 `FILE_STORAGE_ROOT`（默认 `./data`）。

```python
from kit.file_kit import FileKit

FileKit.write_file("docs/a.txt", "hello")         # mode="overwrite"（默认）/ "append"
text = FileKit.read_file("docs/a.txt")
path = FileKit.get_full_path("docs/a.txt")        # -> Path 绝对路径
for root, dirs, files in FileKit.walk_dir("docs"):
    print(root, files)
FileKit.open_with_default_app(path)               # 系统默认程序打开：-> "opened" / "revealed"
```

| 分组 | 方法 |
|------|------|
| 配置 | `configure(root_path) -> None`、`get_root_path() -> str`、`get_full_path(relative_path) -> Path` |
| 文件 | `read_file(relative_path, encoding="utf-8") -> str`、`write_file(relative_path, content, mode="overwrite", encoding="utf-8") -> bool`、`move_file(src, dest) -> bool`、`copy_file(src, dest) -> bool`、`delete_file(relative_path) -> bool`、`file_exists(relative_path) -> bool` |
| 目录 | `move_dir(src, dest) -> bool`、`copy_dir(src, dest) -> bool`、`delete_dir(relative_path) -> bool`、`walk_dir(relative_path="", top_down=True, on_error=None)` |
| 命名 | `generate_unique_id() -> str`、`generate_filename(prefix="", extension="") -> str` |
| 系统 | `open_with_default_app(path) -> OpenResult`（`"opened"` / `"revealed"`，失败降级为文件管理器定位） |

---

## kit.common_kit —— 通用工具

```python
from kit.common_kit import CommonKit

CommonKit.configure(datacenter_id=1, machine_id=1)      # 各限 1–31
CommonKit.generate_snowflake_id()                       # 63 位雪花 ID（int）
CommonKit.generate_snow_id()                            # 雪花 ID（str）
CommonKit.generate_timestamp_id()                       # 20 位时间戳 ID
CommonKit.format_current_time("yyyy-MM-dd")
```

| 方法 | 说明 |
|------|------|
| `configure(datacenter_id, machine_id) -> None` | 配置雪花算法数据中心 / 机器 ID（越界抛 `ValueError`） |
| `generate_snowflake_id(datacenter_id=None, machine_id=None) -> int` | 生成雪花 ID |
| `generate_snow_id(datacenter_id=None, machine_id=None) -> str` | 生成雪花 ID 字符串 |
| `generate_timestamp_id() -> str` | `YYYYMMDDHHMMSSfff` + 3 位随机 |
| `format_current_time(format_pattern="yyyyMMdd") -> str` | 支持 `yyyyMMdd`、`yyyyMMddhhmm`、`yyyyMMddhhmmss`、`yyyy-MM-dd`、`yyyy-MM-dd hh:mm`、`yyyy-MM-dd hh:mm:ss` |
| `reset() -> None` | 重置 ID 生成器状态 |

---

## kit.anydoc_kit —— 文档转 Markdown

安装：`pip install "sdpy-kit[anydoc]"`（支持 `.doc .docx .ppt .pptx .xls .xlsx .csv .pdf`）

```python
from kit.anydoc_kit import AnyDocKit

markdown = AnyDocKit.convert_to_md("report.docx")     # 直接转换，失败抛异常
result = AnyDocKit.convert("report.pdf")              # 安全转换，异常封装为结果对象
if result.success:
    print(result.markdown)
AnyDocKit.convert_to_file("report.docx", "out/report.md")
md = AnyDocKit.convert_bytes_to_md(file_bytes, "csv") # 字节内容直接转，无需落盘
md = AnyDocKit.convert_to_md("scan.pdf", ocr="hosted")  # 需 FIRECRAWL_API_KEY
```

| 方法 | 说明 |
|------|------|
| `is_supported(file_path) -> bool` | 按扩展名判断是否受支持 |
| `detect_format(file_path) -> str \| None` | 由扩展名推断规范格式名 |
| `convert_to_md(file_path, *, ocr="reject", api_key=None, api_url=None) -> str` | 文件 → Markdown（非标 PDF 自动修复重试） |
| `convert_bytes_to_md(data, fmt=None, *, ocr="reject", api_key=None, api_url=None) -> str` | 字节内容 → Markdown |
| `convert(file_path, *, ocr="reject", api_key=None, api_url=None) -> ConvertResult` | 安全转换 |
| `convert_to_file(src_path, dest_path=None, *, ocr="reject", ...) -> str` | 转换并写入 `.md`，返回目标路径 |

`ConvertResult(success, markdown, file_path, source_format, error)`

---

## kit.docx_kit —— Word 模板动态填充

安装：`pip install "sdpy-kit[docx]"`

```python
from kit.docx_kit import DocxKit, DocxReport, DocxSection, DocxParagraph, DocxTable

DocxKit.create_template("templates/report.docx", title="{{ title }}")   # 生成模板骨架
DocxKit.unify_font("templates/report.docx", font_name="宋体")
DocxKit.unify_heading_color("templates/report.docx")

report = DocxReport(
    sections=(
        DocxSection(
            title="第一章 概述",
            level=1,
            blocks=(
                DocxParagraph("本章介绍项目背景。"),
                DocxTable(headers=("名称", "数值"), rows=(("甲", "1"),)),
            ),
        ),
    ),
    variables={"project": "SDPY"},
)
DocxKit.render("templates/report.docx", "out/report.docx", report)
```

| 方法 | 说明 |
|------|------|
| `create_template(dest_path, *, title=None, leading_paragraphs=(), anchor_style=None) -> str` | 生成标准模板骨架（含章节循环锚点） |
| `unify_font(docx_path, *, font_name="宋体") -> str` | 统一文档默认与全部样式的字体（原地，幂等） |
| `unify_heading_color(docx_path, *, color="000000") -> str` | 统一 `Heading 1..9` 字体颜色 |
| `inspect_template(template_path, *, context=None) -> tuple[str, ...]` | 列出模板中未被上下文满足的变量 |
| `render(template_path, dest_path, report, *, anchor_style=None, strict=True) -> str` | 按结构化数据渲染模板并落盘（核心方法） |

数据结构：`DocxReport(sections, variables)`、`DocxSection(title, level=1, blocks=())`、`DocxParagraph(text)`、`DocxListItem(text, level=1, ordered=False)`、`DocxTable(headers, rows=(), style=None)`

---

## kit.md2doc_kit —— Markdown 转 Word / PDF / HTML

安装：`pip install "sdpy-kit[md2doc]"`（PDF 需系统已安装 `mainfont` 指定字体，默认 `SimSun`）

```python
from kit.md2doc_kit import Md2DocKit

Md2DocKit.convert_md_to_docx("report.md", "out.docx", generate_toc=True)
Md2DocKit.convert_md_to_html("report.md", title="项目报告")
Md2DocKit.convert_md_to_pdf("report.md", mainfont="KaiTi")
```

| 方法 | 说明 |
|------|------|
| `convert_md_to_docx(src_path, dest_path=None, *, generate_toc=False, mainfont="宋体", reference_doc=None) -> str` | Markdown → Word（可插目录、统一字体） |
| `convert_md_to_html(src_path, dest_path=None, *, title=None) -> str` | Markdown → 独立 HTML5 文件 |
| `convert_md_to_pdf(src_path, dest_path=None, *, mainfont="SimSun") -> str` | Markdown → PDF（实验性，内部使用） |

> `dest_path=None` 时输出到源文件同目录；同一源文件不支持并发转换。

---

## kit.mineru_kit —— MinerU 文档解析

安装：`pip install "sdpy-kit[mineru]"`（精准解析需 `MINERU_API_KEY`，Agent 轻量解析免登录）

```python
from kit.mineru_kit import MinerUKit

# 精准解析：公网 URL / 本地文件
result = MinerUKit.parse_url("https://cdn-mineru.openxlab.org.cn/demo/example.pdf", model_version="vlm")
if result.state == "done":
    zip_path = MinerUKit.download_zip(result.full_zip_url, "./output")

results = MinerUKit.parse_files(["report1.pdf", "report2.docx"])

# Agent 轻量解析（免登录、单文件 ≤10MB、≤20 页）
r = MinerUKit.agent_parse_url(url, page_range="1-10")
if r.state == "done":
    md_path = MinerUKit.download_markdown(r.markdown_url, "./output")
```

| 分组 | 方法 |
|------|------|
| 精准解析（高层） | `parse_url(url, **opts) -> TaskResult`、`parse_files(file_paths, **opts) -> list[BatchTaskResult]`、`wait_for_task(task_id, *, poll_interval=None, timeout=None)`、`wait_for_batch(batch_id, *, expected_count=None, ...)`、`download_zip(zip_url, dest_dir) -> str` |
| 精准解析（低层） | `create_task(url, **opts) -> str`、`get_task_result(task_id) -> TaskResult`、`create_batch(files, **opts) -> BatchUploadInfo`、`upload_files(file_paths, upload_urls) -> list[bool]`、`get_batch_results(batch_id) -> list[BatchTaskResult]` |
| Agent 轻量 | `agent_parse_url(url, **opts) -> AgentParseResult`、`agent_parse_file(file_path, **opts) -> AgentParseResult`、`agent_create_url_task(url, **opts) -> str`、`agent_create_file_task(file_name, **opts) -> AgentUploadInfo`、`agent_upload_file(file_path, file_url) -> None`、`agent_get_result(task_id)`、`agent_wait_for_result(task_id, *, poll_interval=None, timeout=None)`、`download_markdown(markdown_url, dest_dir, filename=None) -> str` |
| 校验 | `is_supported(file_path) -> bool`、`is_agent_supported(file_path) -> bool` |

> 可选参数（`model_version` / `is_ocr` / `enable_formula` / `enable_table` / `language` / `page_ranges` / `extra_formats` / `no_cache` 等）详见各方法 docstring。
> 结果类型：`TaskResult`、`BatchTaskResult`、`BatchFile`、`BatchUploadInfo`、`AgentParseResult`、`AgentUploadInfo`。

---

## kit.zvec_kit —— 本地向量库

安装：`pip install "sdpy-kit[zvec]"`

```python
from kit.zvec_kit import ZvecKit

schema = ZvecKit.make_text_embedding_schema("docs", dim=768)
ZvecKit.create_collection("docs", schema)
ZvecKit.insert("docs", {"id": "d1", "vectors": {"embedding": [0.1] * 768}, "fields": {"year": 2024}})

results = ZvecKit.vector_query("docs", field="embedding", vector=[0.3] * 768, topk=10)
ZvecKit.flush("docs")
```

| 分组 | 方法 |
|------|------|
| 连接 | `connect() -> bool`、`close() -> None`、`is_connected() -> bool` |
| Collection | `create_collection(name_or_path, schema) -> Collection`、`open_collection(name_or_path)`、`collection_exists(name_or_path) -> bool`、`list_collections() -> list[str]`、`destroy_collection(name_or_path) -> bool`、`get_schema(name_or_path)`、`get_stats(name_or_path) -> dict` |
| 文档 CRUD | `insert(name_or_path, data) -> str`、`insert_multi(name_or_path, data_list) -> list[str]`、`upsert(name_or_path, data) -> str`、`update(name_or_path, data) -> str`、`fetch(name_or_path, ids, *, include_vector=True) -> dict`、`delete(name_or_path, ids) -> bool`、`delete_by_filter(name_or_path, filter_expr) -> bool`、`count(name_or_path) -> int`、`flush(name_or_path) -> None` |
| 检索 / 维护 | `vector_query(name_or_path, *, field, vector, topk=10, filter_expr=None, include_vector=False) -> list[dict]`、`optimize(name_or_path) -> None` |
| Schema | `make_text_embedding_schema(name, dim, metric=None, extra_fields=None) -> CollectionSchema` |

> 文档字典键：`id`（必需）、`vectors`（可选）、`fields`（可选）；`filter_expr` 中相等用单等号 `=`。
> 异常：`ZvecError`（独立于 `kit.db` 的 `DBError` 体系）。

---

## kit.wx_article_kit —— 微信公众号文章抓取存储

零第三方依赖（标准库 `urllib` + 内置 HTML→Markdown 转换器），**无需 extra**，按需 import：

```python
from kit.wx_article_kit import WxArticleKit

# 抓取解析（不落盘）
result = WxArticleKit.fetch("https://mp.weixin.qq.com/s/xxxx")
if result.ok:
    print(result.info.title, result.info.author, result.info.publish_time)

# 抓取并存储：<output_dir>/<file_name>/ 下 <file_name>.md + images/
saved = WxArticleKit.save(url, output_dir="./output", file_name="12345")
print(saved.md_path, saved.images_saved)

# 需要结构化元信息时再产出 meta.json（默认不产出）
WxArticleKit.save(url, "./output", "12345", write_meta=True)
```

| 方法 | 说明 |
|------|------|
| `fetch(url, timeout=20.0) -> ArticleResult` | 抓取并解析正文（不落盘）；结果对象字段 `ok` / `error` / `info`（`ArticleInfo`）/ `markdown` |
| `save(url, output_dir, file_name, write_meta=False, timeout=20.0) -> SavedOutput` | 抓取并存储到 `<output_dir>/<file_name>/`；结果对象字段 `ok` / `error` / `output_dir` / `md_path` / `images_dir` / `title` / `images_saved` |

落盘结构：

```
<output_dir>/<file_name>/
├── <file_name>.md     # 正文 Markdown（YAML 头：title / author / publish_time / source_url）
├── images/            # 图片转存（NNN.ext，正文引用已本地化）
└── meta.json          # 可选：结构化元信息（write_meta=True 时产出）
```

- 元信息默认随 Markdown 的 YAML 头落盘（头部 `publish_time` 为中文日期，如 `2026年8月4日`）；
  `ArticleInfo.publish_time` 与 `meta.json` 内仍为 ISO 8601（结构化数据）。
- 无登录态抓取：命中微信风控（环境异常 / 访问过于频繁）时返回 `ok=False` + 原因，不破解验证码；
  抓取或图片下载失败不抛异常，单张图片失败时正文保留原远程 URL。
- 同名子目录**覆盖写**（不追加序号）；`file_name` 带 `.md` 后缀会自动去除，为空则抛 `ValueError`。

---

## AI / MCP 档案文件

AI 与 MCP 功能需要在项目根目录提供两个 JSON 档案文件：

- `config/ai_models.json` —— chat / embedding / rerank / image 四类型多厂商模型档案
- `config/mcp_servers.json` —— MCP 服务器档案

文件缺失时，首次使用相应功能会报错，**报错信息中附带可直接复制的最小模板**；
其中 `api_key` / `token_env` 字段填写环境变量名，真实密钥配置在 `.env`。

### ai_models.json 可选字段

| 层级 | 字段 | 说明 |
|------|------|------|
| 档案级 | `timeout` | 请求超时秒数（默认 60） |
| 档案级 | `max_tokens` | 输出上限（chat 用）；`AI.chat_ask/chat_complete/chat_stream` 也可按调用点传参，逐级回落：调用点 → 档案 → provider 默认 |
| 档案级 | `retries` | 5xx 重试次数（默认 2；超时不重试，候选链切换才是有效重试） |
| 类型级 | `chain_timeout` | 整链预算秒数（与 `default_profile` 同级）；门面同步等待上限。未配置时按 `sum(候选 timeout) + 10s` 自适应，也可用环境变量 `KIT_AI_CHAIN_TIMEOUT` 全局覆盖 |

### 候选链切换语义（两类，勿混淆）

- **跨调用切换**：`AI.use()` 与其它请求产生的失败冷却均只影响**下一次**请求——每次调用的候选计划在开始时定格；
- **一次调用内降级**：本次调用按开始时定格的计划串行尝试（首帧前失败 / 首帧超时切下一候选），中途的 `use` / 冷却变化不影响它。

全部候选失败（或整链预算耗尽）抛 `AIAggregateError`（`AIRequestError` 子类）：
全候选状态码一致时透传（如整链 429 可用 `exc.status_code == 429` 判定），
429 的 `Retry-After` 解析后挂于 `exc.retry_after`（冷却钳制 5–300s）。

### mcp_servers.json 最小模板

```json
{
  "default_server": "aminer",
  "servers": {
    "aminer": {
      "transport": "streamable-http",
      "url": "https://mcp.aminer.cn/mcp",
      "token_env": "AMINER_TOKEN",
      "timeout": 60
    }
  }
}
```

## License

MIT
