357 lines
28 KiB
Markdown
357 lines
28 KiB
Markdown
# 设计文档 · LLM 接入与迁移地基 / Design · LLM Integration & Migration Foundation
|
||
|
||
> 中英双语。这是「下一轮改动」的总体设计(high-level design),实施步骤见 [`implementation-plan.md`](./implementation-plan.md)。
|
||
> Bilingual. High-level design for the next round of changes; step-by-step plan in [`implementation-plan.md`](./implementation-plan.md).
|
||
>
|
||
> 状态 / Status:**已定稿,待实现 / Agreed, pending implementation**
|
||
> 基线 / Base:`main` @ `b9b6583`
|
||
|
||
---
|
||
|
||
## 0. 本轮范围 / Scope of This Round
|
||
|
||
**本轮只做三件事 / This round delivers exactly three things:**
|
||
|
||
1. **引入 Alembic 数据库迁移系统**(含一层封装,让应用不直接接触 Alembic 细节)。
|
||
Introduce **Alembic** as the migration system (with a thin wrapper so the app never touches Alembic directly).
|
||
2. **LLM 接入**:一个配置页 + 配置落库 + 一个可复用的 LLM 客户端。
|
||
**LLM integration**: a config page + DB-persisted config + a reusable LLM client.
|
||
3. **最基础的 AI 搜索**:搜索页常驻一个「AI 智能搜索」动作,用查询词扩展增强结果。
|
||
**Basic AI search**: a persistent "AI search" action on the search page, powered by query-term expansion.
|
||
|
||
**本轮明确不做(留作未来)/ Explicitly out of scope this round(future):**
|
||
|
||
- 图片内容分析(`image_description` 列、视觉模型调用、手动/批量/夜间生成)。
|
||
Image content analysis (`image_description` columns, vision calls, manual/batch/nightly generation).
|
||
- 向量嵌入 + 相似度语义搜索(AI 搜索的"高阶版")。
|
||
Vector embeddings + similarity semantic search (the "advanced" AI search).
|
||
- 多图、OCR、鉴权、标签系统等(见仓库简报 §15 / see brief §15)。
|
||
|
||
> 架构会为上述未来项**预留接口**(§9),但本轮不实现。
|
||
> The architecture **leaves seams** for the above (§9) without implementing them now.
|
||
|
||
---
|
||
|
||
## 1. 设计原则 / Guiding Principles
|
||
|
||
- **AI 是加分项,不是依赖 / AI is additive, never required.**
|
||
未配置或调用失败时,整站行为与今天**完全一致**。AI 只在"能用且开启"时才介入。
|
||
When unconfigured or on failure, the app behaves **exactly as today**. AI engages only when configured and enabled.
|
||
- **单一 schema 事实来源 / One source of truth for schema.**
|
||
Alembic 接管建表与变更;退休手写的 `_sync_sqlite_image_columns()`。
|
||
Alembic owns schema creation and changes; retire the hand-rolled `_sync_sqlite_image_columns()`.
|
||
- **依赖最小化 / Minimal dependencies.**
|
||
复用已在 `requirements.txt` 的 `httpx` 调 OpenAI 兼容接口;本轮**唯一新增依赖是 `alembic`**。
|
||
Reuse the existing `httpx` for OpenAI-compatible calls; the **only new dependency is `alembic`**.
|
||
- **保持现有形态 / Keep the current shape.**
|
||
仍是 FastAPI + Jinja2 SSR + SQLite,无前端构建链;新页面沿用现有模板风格。
|
||
Still FastAPI + Jinja2 SSR + SQLite, no frontend build; new pages follow existing template style.
|
||
- **测试不联网、数据隔离 / Tests stay offline and isolated.**
|
||
LLM 客户端做成单一可 mock 边界;迁移在测试中真实执行(临时 SQLite)。
|
||
The LLM client is a single mockable boundary; migrations actually run in tests (throwaway SQLite).
|
||
- **可信内网安全姿态 / Trusted-LAN posture.**
|
||
无鉴权(仅内网/VPN 访问);API Key 明文落库为业主在其威胁模型下的明确选择(§7)。
|
||
No auth (LAN/VPN only); plaintext API key in DB is the owner's explicit choice under their threat model (§7).
|
||
|
||
---
|
||
|
||
## 2. 总体架构 / Architecture Overview
|
||
|
||
```text
|
||
┌─────────────────────────────────────────────┐
|
||
HTTP (SSR) │ app/main.py │
|
||
───────────────────► │ 路由 / routes + 请求编排 / orchestration │
|
||
└───┬───────────────┬───────────────┬─────────┘
|
||
│ │ │
|
||
┌────────▼──────┐ ┌──────▼───────┐ ┌──────▼────────┐
|
||
│ app/llm.py │ │ app_settings │ │ 搜索逻辑 │
|
||
│ LLM 客户端 │ │ 读写 helper │ │ AI 检索 seam │
|
||
│ (httpx) │ │ (KV in DB) │ │ (可替换) │
|
||
└───────────────┘ └──────┬───────┘ └───────────────┘
|
||
│
|
||
┌───────────────────▼─────────────────────────┐
|
||
│ app/migrate.py │
|
||
│ 启动 / boot: verify_schema_is_current() 只读 │
|
||
│ └─ 与 head 不一致 → fail-close,拒绝启动 │
|
||
│ 命令 / CLI `python -m app.migrate`(幂等): │
|
||
│ └─ 空库建库 / 认领老库 / upgrade(见 §3) │
|
||
└───────────────────┬─────────────────────────┘
|
||
│ command.upgrade / stamp(仅迁移命令 / migration command only)
|
||
┌───────────────────▼─────────────────────────┐
|
||
│ Alembic (alembic.ini + migrations/) │
|
||
│ V1 baseline → V2(app_settings) → … │
|
||
└───────────────────┬─────────────────────────┘
|
||
│
|
||
┌─────▼─────┐
|
||
│ SQLite │
|
||
└───────────┘
|
||
```
|
||
|
||
新增模块 / New modules:`app/migrate.py`(Alembic 封装)、`app/llm.py`(LLM 客户端)、`migrations/`(Alembic 工程)、`app/templates/settings/`(配置页)。
|
||
改动模块 / Touched:`app/db.py`、`app/main.py`、`app/models.py`、`app/templates/base.html`、`Dockerfile`、`requirements.txt`、`tests/`。
|
||
|
||
---
|
||
|
||
## 3. 迁移子系统 / Migration Subsystem (Alembic)
|
||
|
||
### 3.1 为什么 / Why
|
||
|
||
配置表与未来的新列(如 `tag`、`image_description`)都需要可重复、可审阅的迁移;现有手写列同步只能补图片列,无法长期支撑。
|
||
A config table and future columns need repeatable, reviewable migrations; the hand-rolled column sync only patches image columns and won't scale.
|
||
|
||
### 3.2 收敛不变量 / The Convergence Invariant
|
||
|
||
**所有数据库最终都收敛到同一个 `head`。`V1 baseline` 必须严格等于"今天的真实 schema"(三张表 + 现有图片列),不多一列。**
|
||
All databases converge to the same `head`. The `V1 baseline` must equal **today's actual schema exactly** (the three tables + existing image columns) — nothing more.
|
||
|
||
```text
|
||
迁移链 / chain: V1(baseline = 现状) ──► V2(app_settings) ──► …未来… ──► head
|
||
|
||
老的生产库 / existing prod DB: stamp 到 V1(只写版本号,不建表,不碰数据) ──► upgrade ──► head
|
||
全新/空库 / fresh DB: 跑 V1(真正建三张表) ───────────────────────► upgrade ──► head
|
||
↑ 终点一致 / same end state
|
||
```
|
||
|
||
> `stamp` 只向 `alembic_version` 写一条版本记录,**不执行任何 DDL、不修改数据**。这是安全认领已有库的关键。
|
||
> `stamp` only writes a row into `alembic_version`; it runs **no DDL and touches no data**. This is the key to safely adopting an existing DB.
|
||
|
||
### 3.3 运行时机:校验与迁移分离 / Migrations Run Separately from Startup
|
||
|
||
**关键决策:迁移不在应用启动时发生。** 启动只做**只读校验**,迁移由一个独立、显式的命令/步骤执行。
|
||
**Key decision: migrations do not happen at app startup.** Startup only **verifies** (read-only); migrating is an explicit, separate step.
|
||
|
||
- **启动校验(fail-close)/ Startup check (fail-closed):** `app/db.py::init_db()` 调 `app/migrate.py::verify_schema_is_current(url)`,比较 DB 当前 revision 与 `head`:
|
||
- 一致 → 正常启动 / match → start normally。
|
||
- 不一致(含空库、未认领的老库)→ **fail-close**:输出清晰日志、拒绝提供服务、提示先跑迁移步骤;**不执行任何 DDL、不碰数据**。
|
||
Mismatch (incl. empty or un-adopted DBs) → **fail closed**: clear log, refuse to serve, no DDL, no data change.
|
||
- **迁移命令 / The migration command:** 独立、显式、**幂等**的 `python -m app.migrate`(逻辑在 `app/migrate.py`)。已在 `head` 则空操作并退出 0,便于每次部署都安全重跑。
|
||
A separate, explicit, **idempotent** `python -m app.migrate`. No-op (exit 0) when already at `head`, so it is safe to re-run on every deploy.
|
||
- 退休手写列同步 / Retire the hand-rolled sync:`_sync_sqlite_image_columns()` 删除,schema 由 Alembic 单一接管。
|
||
`_sync_sqlite_image_columns()` is removed; Alembic is the sole owner of schema.
|
||
|
||
为什么 / Why:避免"启动副作用式迁移"、避免多实例并发迁移竞态;当 code 与 DB 不一致时,**宁可不启动也不带病运行**。
|
||
Avoids surprise startup migrations and concurrent-migration races; on a code/DB mismatch it refuses to run rather than run wrong.
|
||
|
||
### 3.4 迁移命令的三种情况 / The Migration Command's Three Cases
|
||
|
||
`python -m app.migrate` 用 SQLAlchemy inspector 判定,分三种:
|
||
`python -m app.migrate` inspects the DB and branches three ways:
|
||
|
||
| 库的状态 / DB state | 动作 / Action |
|
||
| --- | --- |
|
||
| **空库 / empty** | `upgrade head`(建库并升到最新 / create & upgrade to head) |
|
||
| **老库且与 baseline 一致 / existing, matches baseline(2a)** | `stamp V1` → `upgrade head`(认领后升级 / adopt then upgrade) |
|
||
| **老库但与 baseline 不一致 / existing, mismatched(2b)** | **fail-close,不做任何改动 / fail closed, no changes** |
|
||
|
||
> **一致性比对的基准是 baseline(V1),不是 head。** 未认领的老库结构停在 V1(不含 `app_settings` 等后续内容),若拿 head 去比会把合法老库误判为不一致。
|
||
> The match is compared against the **baseline (V1)**, not `head` — an un-adopted DB sits at V1 and would wrongly look "mismatched" if compared against head.
|
||
>
|
||
> ⚠️ SQLite 的 autogenerate 比对存在假阳性(类型亲和、索引命名等),可能让 2b 误 fail。实现上需用**容忍性比对**或允许**人工确认覆盖**(见 §3.6 验证)。
|
||
> SQLite autogenerate has false positives; 2b should use a tolerant comparison or allow a documented manual override (see §3.6).
|
||
|
||
### 3.5 部署形态:Compose db-migration 闸门 / Deployment Shape: a Compose Gate(未来 / future)
|
||
|
||
意图:用一个一次性 `db-migration` 服务跑迁移命令,**成功才放行 App**。本轮可先只交付命令本身,Compose 接线随后。
|
||
Intent: a one-shot `db-migration` service runs the command and **the app starts only on its success**. The command ships this round; the Compose wiring can follow.
|
||
|
||
```yaml
|
||
services:
|
||
db-migration:
|
||
image: <same image>
|
||
command: python -m app.migrate # 成功 exit 0;2b/失败 exit ≠0
|
||
web:
|
||
depends_on:
|
||
db-migration:
|
||
condition: service_completed_successfully
|
||
```
|
||
|
||
迁移失败(含 2b 不一致)→ App 永不启动。
|
||
A failed migration (incl. a 2b mismatch) → the app never starts.
|
||
|
||
### 3.6 Alembic 配置要点 / Alembic config notes
|
||
|
||
- `migrations/env.py`:`target_metadata = Base.metadata`;DB URL 从 `get_settings().database_url` 动态读取(不写死在 `alembic.ini`);对 SQLite 设 `render_as_batch=True`(便于未来改列/删列走 batch 模式)。
|
||
`target_metadata = Base.metadata`; URL read dynamically from settings; `render_as_batch=True` for SQLite.
|
||
- **V1 baseline 的生成与验证 / Authoring & verifying V1:** 用当前 models 对**空库** autogenerate 得到完整建表脚本;再对**生产库副本**跑 `alembic check`,**应显示无差异**——即印证"schema 符合预期、可安全盖章"。
|
||
Autogenerate against an empty DB for the full create script; then run `alembic check` against a copy of the prod DB — it **should report no diff**, confirming it's safe to stamp.
|
||
- 镜像 / Image:`Dockerfile` 需 `COPY` `alembic.ini` 与 `migrations/`,否则容器内无迁移脚本。
|
||
- CI(可选 / optional):加一步 `alembic check`,防止改了 model 却忘记生成迁移。
|
||
Add an `alembic check` step to catch model/migration drift.
|
||
|
||
---
|
||
|
||
## 4. LLM 接入 / LLM Integration
|
||
|
||
### 4.1 配置存储:键值表 / Config storage: a KV table
|
||
|
||
新增表 `app_settings(key TEXT PRIMARY KEY, value TEXT)`(由 V2 迁移创建)。
|
||
New table `app_settings(key TEXT PRIMARY KEY, value TEXT)` (created by the V2 migration).
|
||
|
||
**为什么用 KV 而非定型列 / Why KV instead of typed columns:** 后续还会陆续加配置项;给*已有表*加列有迁移成本,而 KV 加配置项=加一行,永不迁移。类型与校验在 Python 侧处理。
|
||
More settings are coming; adding columns to an *existing* table costs a migration, whereas a KV row never does. Typing/validation live in Python.
|
||
|
||
本轮使用的 key / Keys used this round:
|
||
|
||
| key | 含义 / Meaning | 默认 / Default |
|
||
| --- | --- | --- |
|
||
| `llm_enabled` | LLM 总开关 / master toggle | `false` |
|
||
| `llm_base_url` | OpenAI 兼容端点 / endpoint | `https://api.openai.com/v1` |
|
||
| `llm_model` | 模型名 / model name | (空 / empty) |
|
||
| `llm_api_key` | API Key(明文 / plaintext,见 §7) | (空 / empty) |
|
||
| `ai_search_enabled` | AI 搜索功能开关 / AI-search feature toggle | `false` |
|
||
| `ai_search_extra_hints` | AI 搜索:可选「额外领域提示」,追加到默认系统提示词(step 3 引入)/ optional extra domain hints appended to the default prompt | (空 / empty) |
|
||
|
||
> 读写封装 / Access helpers:`get_app_settings(db) -> LLMConfig`(dataclass 视图)与 `save_app_settings(db, ...)`,供路由与 `app/llm.py` 复用。
|
||
> Helpers `get_app_settings(db) -> LLMConfig` and `save_app_settings(db, ...)`, reused by routes and `app/llm.py`.
|
||
|
||
### 4.2 LLM 客户端 / The client (`app/llm.py`)
|
||
|
||
OpenAI 兼容的薄客户端,基于 `httpx`,**无新依赖** / A thin OpenAI-compatible client over `httpx`, **no new dependency**:
|
||
|
||
- `is_configured(cfg) -> bool`:开关开启且 `model`/`api_key` 齐全。
|
||
- `test_connection(cfg) -> Result`:发一个最小请求验证 `base_url`/`model`/`api_key`,供配置页"测试连接"用。
|
||
- `expand_query(cfg, query, extra_hints="") -> ExpansionResult`:把查询词扩成一批近义/相关词;`terms` 为扩展词列表(不含原词),`error` 用于区分超时/网络/HTTP 等真实调用失败(提示词与输出契约见 §5.2)。
|
||
- `analyze_image(...)`:**本轮不实现**,仅在文档中预留为未来接口(图片分析轮次)。Reserved for a future round, not implemented now.
|
||
|
||
要点 / Notes:
|
||
- 统一超时与错误处理;失败不抛到用户面前,按"优雅降级"返回可识别的失败信号。
|
||
Unified timeout + error handling; failures degrade gracefully rather than surfacing as 500s.
|
||
- 同步实现即可——FastAPI 把同步 `def` 路由丢线程池执行,阻塞式 httpx 调用可接受。
|
||
A synchronous implementation is fine — FastAPI runs sync handlers in a threadpool.
|
||
- **唯一对外/网络边界**,测试中整体 mock,CI 保持无网络。
|
||
The **single network boundary**, fully mocked in tests.
|
||
|
||
### 4.3 配置页 / Config page
|
||
|
||
| 路由 / Route | 作用 / Purpose |
|
||
| --- | --- |
|
||
| `GET /settings` | 渲染配置表单(Key 脱敏显示)/ render form (key masked) |
|
||
| `POST /settings` | 保存配置到 `app_settings` / persist to `app_settings` |
|
||
| `POST /settings/test` | 用当前/待保存配置测试连接 / test connection |
|
||
|
||
- 模板 `app/templates/settings/form.html`,沿用现有卡片/表单样式;`base.html` 顶部导航加一个「设置」入口。
|
||
Template under `settings/`, reusing existing styles; add a "设置/Settings" link in `base.html` nav.
|
||
- **Key 脱敏 / Key masking**:页面不回显明文,显示「已配置,留空=不修改」,提交留空则保留原值。
|
||
Never echo the plaintext key; show "configured, leave blank to keep", and keep the old value if left blank.
|
||
|
||
### 4.4 降级 / Degradation
|
||
|
||
`llm_enabled` 关或未配置时:配置页照常可用;AI 搜索按钮隐藏或提示去配置;其余功能与现状一致。
|
||
When disabled/unconfigured: the settings page still works; the AI-search button is hidden or hints to configure; everything else is unchanged.
|
||
|
||
---
|
||
|
||
## 5. AI 搜索 / AI Search
|
||
|
||
### 5.1 行为 / Behavior
|
||
|
||
- **常驻动作 / Persistent action:** 搜索页**始终**提供「AI 智能搜索」,**不以"零结果"为前提**——即便普通搜索已出结果,用户不满意时也能点。
|
||
The "AI search" action is **always** present on the search page, **not gated on zero results** — usable even when normal results exist.
|
||
- **流程 / Flow:** 普通 `LIKE` 照常先出结果 → 用户触发 AI → `expand_query` 返回 `ExpansionResult`(扩展词 `terms` 不含原词;调用失败写入 `error`)→ `ai_search` 用「原词 + 扩展词」对 `name`/`note` 做 OR `LIKE` 重搜 → 展示,并用横幅标注「AI 帮你扩展了:…」。
|
||
Normal `LIKE` first → user triggers AI → `expand_query` returns an `ExpansionResult` (`terms` exclude the original query; failures are represented by `error`) → `ai_search` OR-`LIKE`s over name/note with the original + expanded terms → render with a banner listing the expansion.
|
||
- **只把查询词发出去 / Only the query leaves**,不外泄物品清单;token 恒定、不随上千件物品增长。
|
||
Only the query is sent; the inventory is not. Token cost is constant and does not grow with thousands of items.
|
||
|
||
### 5.2 提示词与输出契约 / Prompt & Output Contract
|
||
|
||
`expand_query` 的**质量**取决于提示词,**集成稳定性**取决于输出契约——两者都在代码侧掌控(决策 C)。
|
||
Quality hinges on the prompt; integration stability hinges on the output contract — both are code-controlled (decision C).
|
||
|
||
- **基础系统提示词写死在 `app/llm.py`(用户改不坏)/ Base system prompt hardcoded:** 框定搬家/家居场景,要求"列出用户可能用来命名同一物品的相关词(近义、别称、上位类别、具体品类)";语言跟随查询;最多约 8 个;不解释、不造无关词。
|
||
Frames the moving/household domain, asks for related naming terms, follows the query's language, caps the count, no prose.
|
||
- **可选「额外领域提示」/ Optional extra hints:** KV `ai_search_extra_hints`(设置页一个多行输入,默认空)。非空时**追加**到基础提示词之后,供业主微调倾向(如"厨房用品多,偏向厨具类")。**它只能补充,不能改写输出格式。**
|
||
An optional free-text setting appended to the base prompt; it can only add guidance, never alter the output format.
|
||
- **输出契约(代码强制,与提示词解耦)/ Output contract (code-enforced):** 要求模型只返回 **JSON 字符串数组**;解析时去掉 ` ```json ` 围栏 → `json.loads` → 只接受字符串数组 → 过滤空串/过长词 → 最多 8 个。散文、坏 JSON、JSON object、非字符串数组都视为**合法空扩展**(`terms=[]`, `error=None`);网络错误、HTTP 错误、超时等真实调用失败写入 `ExpansionResult.error`。`expand_query` 的 `terms` 只包含扩展词;**原词由 `ai_search` 并入并去重**。
|
||
Require a JSON string array; strip code fences, `json.loads`, accept only string arrays, filter empty/overlong terms, and cap to 8 terms. Prose, bad JSON, JSON objects, and non-string arrays are successful empty expansions (`terms=[]`, `error=None`); network/HTTP/timeout failures are represented by `ExpansionResult.error`. `expand_query.terms` contains only expanded terms; `ai_search` adds the original term and dedupes.
|
||
- **客户端参数 / Client params:** 低 temperature、较小 max_tokens、设超时。Low temperature, small max_tokens, a timeout.
|
||
- **措辞留松 / Wording left loose:** 默认提示词的具体字句可在 step-3 实测中迭代,不在文档里冻死。
|
||
Exact default wording can be iterated during step-3 testing.
|
||
|
||
### 5.3 实现接口 / Implementation seam
|
||
|
||
- 路由层扩展现有 `GET /search`:增加 `ai=1` 触发位(如 `GET /search?q=锅&ai=1`),保持单页、可收藏、SSR 友好。
|
||
Extend the existing `GET /search` with an `ai=1` trigger (e.g. `/search?q=…&ai=1`), staying single-page and bookmarkable.
|
||
- 内部定义可替换的检索 seam,例如 `ai_search(db, query) -> (expanded_terms, results, error_message)`:
|
||
Define a replaceable retrieval seam, e.g. `ai_search(db, query) -> (expanded_terms, results, error_message)`:
|
||
- **本轮 / now:** 内部=查询词扩展 + 本地 `LIKE`。
|
||
- **未来 / later:** 换成向量嵌入 + 相似度检索,**路由与模板不变**。
|
||
Swap to embeddings + similarity later **without changing the route or template**.
|
||
- 本轮检索范围=`name` + `note`(`image_description` 本轮不存在)。
|
||
Search scope this round = `name` + `note` (no `image_description` yet).
|
||
|
||
### 5.4 降级 / Degradation
|
||
|
||
AI 关闭/未配置 → 不显示按钮(或提示去 `/settings`);调用失败 → 友好提示并回退到普通结果。
|
||
AI off/unconfigured → no button (or a hint to `/settings`); on failure → a friendly message, fall back to normal results.
|
||
|
||
合法空扩展(模型返回 `[]` 或输出无法通过严格 JSON 字符串数组契约)不视为调用失败:回退普通结果,不显示故障提示。
|
||
A legitimate empty expansion (model returns `[]` or output fails the strict JSON-string-array contract) is not treated as a call failure: fall back to normal results without an error banner.
|
||
|
||
---
|
||
|
||
## 6. 数据模型与路由变更 / Data Model & Route Changes
|
||
|
||
**数据模型 / Data model(本轮):**
|
||
- 新增 `AppSetting`(表 `app_settings`,KV)。由 V2 迁移建表。
|
||
Add `AppSetting` (`app_settings`, KV), created by the V2 migration.
|
||
- `boxes` / `items` / `subitems` **本轮不变**。Unchanged this round.
|
||
|
||
**新增/改动路由 / Routes added/changed:**
|
||
- `GET /settings`、`POST /settings`、`POST /settings/test`(新)。
|
||
- `GET /search?q=&ai=1`(扩展现有)。
|
||
- `base.html` 导航新增「设置」。
|
||
|
||
---
|
||
|
||
## 7. 安全姿态 / Security Posture
|
||
|
||
- **无鉴权 / No auth**:仅经可信内网 / VPN + nginx HTTPS 访问,业主已确认风险可接受。
|
||
LAN/VPN + nginx HTTPS only; owner accepts the risk.
|
||
- **API Key 明文落库 / Plaintext API key in DB**:业主明确选择。理由:备份经 `rclone` 至业主自有 OneDrive,链路可信;若攻击者已能读到服务器文件,则任何落盘位置都不安全。
|
||
Owner's explicit choice; backups go via `rclone` to the owner's own OneDrive, and a server-file-read attacker defeats any at-rest location anyway.
|
||
- **UI 不回显明文 Key / UI never echoes the key**(§4.3)——这是表单卫生,不是加密。
|
||
- **外发数据 / Data egress**:AI 搜索只发送*查询词*;图片分析(未来)才会外发图片。
|
||
AI search sends only the *query*; image egress only arrives with the future image-analysis feature.
|
||
|
||
---
|
||
|
||
## 8. 测试策略 / Testing Strategy
|
||
|
||
- **迁移在测试中真实执行 / Migrations run in tests:** fixture 先在临时 SQLite 上跑迁移命令(建库 → `upgrade head`),再 `create_app()`(启动校验随之通过)。schema 来自迁移本身——单一事实来源 + 迁移覆盖。
|
||
The fixture runs the migration command on a tmp SQLite first, then `create_app()` (whose startup check then passes).
|
||
- **认领逻辑测试 / Adoption test(2a):** 构造"有 `boxes` 数据但无 `alembic_version`"的库 → 跑迁移命令 → 断言数据保留、版本到达 head。
|
||
Build a "has `boxes` data, no `alembic_version`" DB → run the migration command → assert data preserved and version at head.
|
||
- **fail-close 测试 / Fail-closed tests:** ① DB 未到 head 时 `create_app()` 启动应 fail-close;② 2b 不一致时迁移命令应 fail-close 且不改动。
|
||
① `create_app()` fails closed when the DB is not at head; ② the migration command fails closed (and changes nothing) on a 2b mismatch.
|
||
- **LLM 全程 mock / Mock the LLM:** 打桩 `expand_query` / `test_connection`(或底层 httpx),CI 不联网。
|
||
- **新增用例 / New cases:** 配置增删改 + Key 脱敏;测试连接(mock);AI 搜索扩展命中;各降级路径(未配置/失败)。
|
||
|
||
---
|
||
|
||
## 9. 未来扩展(本轮不做,但已预留)/ Future Extensions (seams reserved)
|
||
|
||
| 未来项 / Future item | 预留点 / Seam already in place |
|
||
| --- | --- |
|
||
| 图片内容分析 / Image analysis | `app/llm.py` 预留 `analyze_image`;迁移系统可加 `image_description` 列;搜索范围可纳入该列。<br>`analyze_image` reserved; migrations can add `image_description`; search can include it. |
|
||
| 向量语义搜索 / Vector semantic search | `ai_search(...)` seam 可整体替换;批处理可与图片描述补算共用。<br>The `ai_search` seam is swappable; batch jobs can be shared. |
|
||
| 夜间批处理 / Nightly batch | 分析逻辑写成批量友好函数,cron 仅是薄包装(仿 backup cron)。<br>Batch-friendly functions; cron is a thin wrapper like the backup cron. |
|
||
| 文本/视觉模型分离 / Split models | `app_settings` 加一个 key 即可,无需迁移。<br>Add one KV key, no migration. |
|
||
|
||
---
|
||
|
||
## 10. 决策记录 / Decisions Log
|
||
|
||
| # | 决策 / Decision | 理由 / Rationale |
|
||
| --- | --- | --- |
|
||
| D1 | 先引入 Alembic 再做功能 / Alembic before features | 配置表与未来列都依赖可靠迁移;退休手写列同步。 |
|
||
| D2 | V1 baseline 严格等于现状,新东西放 V2+ / baseline = current schema only | 使 `stamp` 认领老库为真、安全。 |
|
||
| D3 | 迁移与启动分离:启动只校验 + fail-close,迁移走独立幂等命令(`python -m app.migrate`)/ 未来 Compose `db-migration` 闸门 / migrations separated from startup | 避免启动副作用式迁移与并发竞态;schema 不一致宁可不启动也不带病运行;迁移成功才放行 App。 |
|
||
| D4 | 配置用 KV 表 / KV settings table | 后续配置项多,避免反复给已有表加列。 |
|
||
| D5 | API Key 明文落库 / plaintext key | 业主威胁模型下可接受;备份至自有 OneDrive。 |
|
||
| D6 | 复用 httpx,手搓 OpenAI 调用 / reuse httpx | 不引入 `openai` SDK,依赖最小。 |
|
||
| D7 | AI 搜索常驻、不依赖零结果 / persistent AI search | 用户对已有结果不满意时也能用。 |
|
||
| D8 | AI 搜索 v1=查询词扩展 / query-term expansion | 上千件物品下可扩展、不外泄清单、token 恒定。 |
|
||
| D9 | 检索做成可替换 seam / pluggable retrieval | 未来换嵌入式语义搜索时上层不动。 |
|
||
| D10 | 图片分析不在本轮 / image analysis deferred | 业主本轮三件事不含它;架构预留接口。 |
|
||
| D11 | AI 搜索提示词:默认写死 + 可选「额外领域提示」;输出契约由代码强制 / hardcoded default prompt + optional extra-hints, code-enforced JSON contract | 保证解析稳定(用户改不坏),又给业主一点不改代码即可微调的空间。 |
|