docs: add LLM integration design and three-step implementation plan
Add docs/: a bilingual repository brief, plus docs/design/ with the high-level design (Alembic migration foundation, LLM integration, basic AI search) and a self-contained per-step implementation plan (step 1-3).
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# docs/design · 设计文档 / Design Docs
|
||||
|
||||
本目录存放面向具体改动轮次的设计与实施计划。
|
||||
Design and implementation plans for specific rounds of changes.
|
||||
|
||||
## 当前轮次 / Current round — LLM 接入与迁移地基 / LLM Integration & Migration Foundation
|
||||
|
||||
本轮三件事 / Three deliverables:① 引入 Alembic 迁移系统(含封装)② LLM 接入(配置页 + 落库 + 客户端)③ 基础 AI 搜索(查询词扩展)。
|
||||
**本轮不含图片分析**(留作未来,架构已预留接口)。Image analysis is **not** in this round (reserved for the future).
|
||||
|
||||
**总体设计(High-level,"做什么/为什么")/ High-level design ("what/why"):**
|
||||
|
||||
| 文件 / File | 内容 / Contents |
|
||||
| --- | --- |
|
||||
| [`llm-integration-design.md`](./llm-integration-design.md) | 原则、架构、迁移子系统、LLM 接入、AI 搜索、安全、测试、未来扩展、决策记录(D1–D10)。<br>Principles, architecture, migration subsystem, LLM, AI search, security, testing, future seams, decisions log. |
|
||||
|
||||
**实施计划("怎么做",每步一个自包含文件)/ Implementation plan ("how", one self-contained file per step):**
|
||||
|
||||
| 文件 / File | 内容 / Contents |
|
||||
| --- | --- |
|
||||
| [`implementation-plan.md`](./implementation-plan.md) | 总览:步骤顺序、依赖、跨步骤约定。<br>Overview: sequence, dependencies, cross-cutting conventions. |
|
||||
| [`step-1-alembic-foundation.md`](./step-1-alembic-foundation.md) | 步骤 1:Alembic 迁移地基(不改 schema)。<br>Step 1: migration foundation. |
|
||||
| [`step-2-llm-integration.md`](./step-2-llm-integration.md) | 步骤 2:LLM 接入(`app_settings` + 客户端 + 配置页)。<br>Step 2: LLM integration. |
|
||||
| [`step-3-ai-search.md`](./step-3-ai-search.md) | 步骤 3:基础 AI 搜索(查询词扩展)。<br>Step 3: basic AI search. |
|
||||
|
||||
> 每个 step 文件**自包含**:实现 Agent 每次只读对应的一个文件即可执行。
|
||||
> Each step file is **self-contained** — an implementation agent only needs to read that one file.
|
||||
|
||||
> 实现与设计若有偏差,请回写本目录,并同步仓库简报 `../repository-brief.md`(尤其 §10 迁移、§15 约束)。
|
||||
> If implementation diverges, update these docs and the brief (`../repository-brief.md`, esp. §10 & §15).
|
||||
@@ -0,0 +1,36 @@
|
||||
# 实施计划 · 总览 / Implementation Plan · Overview
|
||||
|
||||
> 配合设计文档 [`llm-integration-design.md`](./llm-integration-design.md) 阅读。
|
||||
> Read alongside the high-level design doc.
|
||||
>
|
||||
> 三步走,**每步一个独立文件、一个可独立合入的 PR / branch**。实现 Agent 每次只需读对应的 step 文件即可执行。
|
||||
> Three steps, **one self-contained file and one mergeable PR per step**. An implementation agent only needs to read the relevant step file.
|
||||
|
||||
---
|
||||
|
||||
## 步骤与文件 / Steps & Files
|
||||
|
||||
| 步骤 / Step | 文件 / File | 目标 / Goal | 改 schema? | 依赖 / Depends on |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **1** | [`step-1-alembic-foundation.md`](./step-1-alembic-foundation.md) | Alembic 迁移地基(封装 + V1 baseline + 自动认领),**不改 schema** | 否 / No | — |
|
||||
| **2** | [`step-2-llm-integration.md`](./step-2-llm-integration.md) | LLM 接入:`app_settings` 表 + 客户端 + 配置页 | 是 / Yes (V2) | 步骤 1 / Step 1 |
|
||||
| **3** | [`step-3-ai-search.md`](./step-3-ai-search.md) | 基础 AI 搜索:常驻按钮 + 查询词扩展 | 否 / No | 步骤 2 / Step 2 |
|
||||
|
||||
**顺序 / Sequence:** 严格按 1 → 2 → 3,前一步绿了再进下一步。
|
||||
Strictly 1 → 2 → 3; advance only when the previous step is green.
|
||||
|
||||
---
|
||||
|
||||
## 跨步骤约定 / Cross-cutting Conventions(每步都适用 / apply to every step)
|
||||
|
||||
- **提交 / Commits:** 每步独立 branch + PR;遵循仓库约定——**不主动 push/commit,除非业主明确要求**。
|
||||
One branch/PR per step; **do not push/commit unless explicitly asked**.
|
||||
- **CI 不联网 / Network-free CI:** 任何 LLM 调用在测试中必须 mock。
|
||||
All LLM calls must be mocked in tests.
|
||||
- **降级优先 / Degradation first:** 每个 AI 接入点先想清楚"未配置 / 调用失败"时的表现;AI 是加分项,不是依赖。
|
||||
Always design the "unconfigured / failed" path first; AI is additive, never required.
|
||||
- **依赖最小 / Minimal deps:** 复用已有 `httpx`;本轮唯一新增依赖是 `alembic`。不要引入 `openai` SDK。
|
||||
Reuse `httpx`; the only new dependency this round is `alembic`. Do not add the `openai` SDK.
|
||||
- **保持形态 / Keep the shape:** FastAPI + Jinja2 SSR + SQLite,无前端构建链;新页面沿用现有模板/样式。
|
||||
- **文档同步 / Keep docs in sync:** 实现与设计若有偏差,回写本目录对应文件与仓库简报 `../repository-brief.md`(§10 迁移、§15 约束)。
|
||||
If implementation diverges, update the step file, the design doc, and the brief (§10/§15).
|
||||
@@ -0,0 +1,303 @@
|
||||
# 设计文档 · 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(封装层) │
|
||||
│ run_migrations(url): 自动 stamp / upgrade │
|
||||
└───────────────────┬─────────────────────────┘
|
||||
│ command.upgrade / stamp
|
||||
┌───────────────────▼─────────────────────────┐
|
||||
│ 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 自动认领逻辑 / Auto-adoption (in `app/migrate.py`)
|
||||
|
||||
`init_db()` 启动时调用 `run_migrations(url)`,内部用 SQLAlchemy inspector 判断:
|
||||
At startup `init_db()` calls `run_migrations(url)`, which inspects the DB:
|
||||
|
||||
| 库的状态 / DB state | 动作 / Action |
|
||||
| --- | --- |
|
||||
| 有 `alembic_version` / has `alembic_version` | `upgrade head` |
|
||||
| 无 `alembic_version` 但有 `boxes` 表(=老生产库)/ no `alembic_version` but `boxes` exists | `stamp V1` → `upgrade head` |
|
||||
| 全空 / empty | `upgrade head`(从 V1 建起 / build from V1) |
|
||||
|
||||
这样**生产机重新部署零手动迁移命令**,老数据安全。
|
||||
So redeploying the production box needs **zero manual migration commands**; existing data is safe.
|
||||
|
||||
### 3.4 封装层 / The Wrapper (`app/migrate.py`)
|
||||
|
||||
应用其余部分只调用 `run_migrations(database_url)`,不直接接触 Alembic API。封装内部:
|
||||
The rest of the app only calls `run_migrations(database_url)`; Alembic stays encapsulated. Internally it:
|
||||
|
||||
- 以编程方式构造 Alembic `Config`(`script_location` 指向打包进镜像的 `migrations/`,`sqlalchemy.url` 用传入的 URL)。
|
||||
Builds an Alembic `Config` programmatically (`script_location` → bundled `migrations/`, `sqlalchemy.url` → the passed URL).
|
||||
- 调 `command.stamp(...)` / `command.upgrade(...)`。
|
||||
- 由 `init_db()` 调用,取代原来的 `create_all()` + `_sync_sqlite_image_columns()`(后者删除)。
|
||||
Called by `init_db()`, replacing `create_all()` + `_sync_sqlite_image_columns()` (the latter is removed).
|
||||
|
||||
### 3.5 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` |
|
||||
|
||||
> 读写封装 / 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) -> list[str]`:把查询词扩成一批近义/相关词(本轮 AI 搜索用,见 §5)。
|
||||
- `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` 把查询词扩成近义/相关词 → 用「原词 + 扩展词」对 `name`/`note` 做 OR `LIKE` 重搜 → 展示,并用横幅标注「AI 帮你扩展了:…」。
|
||||
Normal `LIKE` first → user triggers AI → `expand_query` → OR-`LIKE` 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 实现接口 / 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)`:
|
||||
Define a replaceable retrieval seam, e.g. `ai_search(db, query) -> (expanded_terms, results)`:
|
||||
- **本轮 / 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.3 降级 / Degradation
|
||||
|
||||
AI 关闭/未配置 → 不显示按钮(或提示去 `/settings`);调用失败 → 友好提示并回退到普通结果。
|
||||
AI off/unconfigured → no button (or a hint to `/settings`); on failure → a friendly message, fall back to normal results.
|
||||
|
||||
---
|
||||
|
||||
## 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:** 临时 SQLite 上 `upgrade head`,schema 来自迁移本身——单一事实来源,且为迁移提供覆盖。
|
||||
`upgrade head` on a tmp SQLite; schema comes from migrations — single source of truth plus migration coverage.
|
||||
- **认领逻辑测试 / Adoption test:** 构造一个"有 `boxes` 数据但无 `alembic_version`"的库,跑 `run_migrations`,断言数据保留且版本到达 head。
|
||||
Build a "has `boxes` data, no `alembic_version`" DB, run `run_migrations`, assert data preserved and version at head.
|
||||
- **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 | 自动 stamp/upgrade / auto-adopt | 生产机零手动迁移;契合自托管"开箱即用"。 |
|
||||
| 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 | 业主本轮三件事不含它;架构预留接口。 |
|
||||
@@ -0,0 +1,101 @@
|
||||
# 步骤 1 · Alembic 迁移地基 / Step 1 · Migration Foundation
|
||||
|
||||
> **可独立执行 / Self-contained.** 完整背景见设计文档 [`llm-integration-design.md`](./llm-integration-design.md) §3;跨步骤约定见 [`implementation-plan.md`](./implementation-plan.md)。
|
||||
> **前置 / Prerequisite:** 无(第一步)/ none.
|
||||
> **产出 / Output:** 一个可独立合入的 PR;**不改任何业务 schema**。A mergeable PR with **zero business-schema change**.
|
||||
|
||||
---
|
||||
|
||||
## 目标 / Goal
|
||||
|
||||
引入 Alembic 并**安全接管现有生产库**,schema 一点不改,所有现有测试保持绿。
|
||||
Introduce Alembic and **safely adopt the existing prod DB**, with zero schema change; all existing tests stay green.
|
||||
|
||||
---
|
||||
|
||||
## 必要背景 / Essential Context(仅凭本文件即可执行 / enough to execute from this file)
|
||||
|
||||
- **当前没有 Alembic。** 唯一的"迁移"是 `app/db.py::_sync_sqlite_image_columns()`(启动时缺图片列就 `ALTER TABLE ADD COLUMN`)。
|
||||
No Alembic today; the only "migration" is the hand-rolled image-column sync in `app/db.py`.
|
||||
- `app/db.py::init_db()` 在 FastAPI lifespan 启动时被 `create_app()` 调用,现在执行 `Base.metadata.create_all()` + `_sync_sqlite_image_columns()`。相关符号:`Base`、`engine`、`SessionLocal`、`configure_database()`。
|
||||
`init_db()` runs at lifespan startup and currently does `create_all()` + the image-column sync.
|
||||
- `tests/conftest.py` 的 `client` fixture:`configure_database(tmp_url)` → `create_app()`(触发 `init_db`)。每个测试用临时 SQLite,互不污染。
|
||||
- models 在 `app/models.py`:`Box` / `Item` / `SubItem` 三张表;每张含 `image_blob`(BLOB) / `image_mime_type` / `image_width` / `image_height`,以及 `created_at` / `updated_at`。
|
||||
- DB URL 来自 `app/config.py::get_settings().database_url`(默认 `sqlite:///./data/app.db`)。
|
||||
- **生产库**是当年 `create_all` 建的、**已装上千件数据、没有 `alembic_version` 表**。
|
||||
|
||||
### 铁律 / The Invariant(不可违背 / non-negotiable)
|
||||
|
||||
- 所有数据库最终收敛到同一个 `head`。All DBs converge to the same `head`.
|
||||
- **V1 baseline 必须严格等于"今天的真实 schema"**(三张表 + 现有图片列 + 索引),**不多一列**。新东西放后续 revision。
|
||||
The V1 baseline must equal **today's actual schema exactly** — nothing more.
|
||||
- 老库:`stamp V1`(只写版本号,**不建表、不碰数据**)→ `upgrade head`。
|
||||
Existing DB: `stamp V1` (writes only the version row, **no DDL, no data change**) → `upgrade head`.
|
||||
- 新库:跑 `V1`(真正建表)→ `upgrade head`。
|
||||
Fresh DB: run `V1` (creates tables) → `upgrade head`.
|
||||
|
||||
---
|
||||
|
||||
## 任务 / Tasks
|
||||
|
||||
- [ ] `requirements.txt` 增加 `alembic`(钉一个明确版本 / pin a version)。
|
||||
- [ ] 初始化 Alembic 工程:`alembic.ini` + `migrations/`(含 `env.py`、`versions/`)。
|
||||
- [ ] 配置 `migrations/env.py`:
|
||||
- `target_metadata = app.db.Base.metadata`(确保导入 `app.models` 以注册三张表)。
|
||||
- `sqlalchemy.url` **从 `app.config.get_settings().database_url` 动态读取**,不写死在 `alembic.ini`。
|
||||
- 对 SQLite 设 `render_as_batch=True`(为未来改列/删列预留 batch 能力)。
|
||||
- [ ] 生成 **V1 baseline 迁移**=当前 models 的完整建表(`boxes`/`items`/`subitems`,含图片列与索引)。做法:对**空库** `--autogenerate`。
|
||||
Author V1 by autogenerating against an **empty** DB.
|
||||
- [ ] **验证 baseline**:对一份**生产库副本**跑 `alembic check`,确认**无差异**(印证可安全 `stamp`;SQLite 偶有类型亲和/索引命名假差异,人眼复核)。
|
||||
Verify with `alembic check` against a **copy of the prod DB** → expect no diff.
|
||||
- [ ] 新增封装 `app/migrate.py`,导出 `run_migrations(database_url: str)`:
|
||||
- 编程方式构造 Alembic `Config`(`script_location` 指向打包进镜像的 `migrations/`,`sqlalchemy.url` = 传入 URL)。
|
||||
- 用 SQLAlchemy inspector 实现自动认领:
|
||||
- 有 `alembic_version` → `command.upgrade(cfg, "head")`
|
||||
- 无 `alembic_version` 但有 `boxes` 表 → `command.stamp(cfg, "<V1 rev>")` → `command.upgrade(cfg, "head")`
|
||||
- 全空 → `command.upgrade(cfg, "head")`
|
||||
- [ ] 改 `app/db.py::init_db()`:改为调 `run_migrations(resolved_url)`,**删除** `_sync_sqlite_image_columns()`(Alembic 接管后冗余)。保留 `configure_database()` / engine 装配逻辑。
|
||||
`init_db()` calls `run_migrations(...)`; **remove** `_sync_sqlite_image_columns()`.
|
||||
- [ ] `Dockerfile`:加 `COPY alembic.ini .` 与 `COPY migrations ./migrations`(否则容器内无迁移脚本)。
|
||||
- [ ] CI(可选 / optional):`.github/workflows/test.yml` 加一步 `alembic check`,防止 model 与迁移漂移。
|
||||
|
||||
---
|
||||
|
||||
## 涉及文件 / Files
|
||||
|
||||
`requirements.txt`、`alembic.ini`(新)、`migrations/**`(新)、`app/migrate.py`(新)、`app/db.py`、`Dockerfile`、`tests/`、(可选)`.github/workflows/test.yml`。
|
||||
|
||||
---
|
||||
|
||||
## 测试 / Tests
|
||||
|
||||
- [ ] 现有 ~83 个测试全绿(它们经 `init_db` 现在改走迁移建表)。
|
||||
All existing ~83 tests pass (schema now built via migrations through `init_db`).
|
||||
- [ ] 新增**认领老库**用例:构造一个"有 `boxes` 数据、无 `alembic_version`"的库(可先用 `create_all` 造),调 `run_migrations` 后断言:数据保留、`alembic_version` 到达 `head`、未重复建表报错。
|
||||
New adoption test: a "has `boxes` data, no `alembic_version`" DB → after `run_migrations`, data preserved and version at `head`.
|
||||
- [ ] 新增**全新库**用例:空 URL → `run_migrations` 后三张表存在、版本到 `head`。
|
||||
|
||||
---
|
||||
|
||||
## 验收 / Acceptance
|
||||
|
||||
- 全新库:从 V1 建表,应用正常起。Fresh DB builds from V1; app starts.
|
||||
- 模拟老库:自动 `stamp` + `upgrade`,**数据无损**。Existing-like DB auto-adopts; data intact.
|
||||
- 全部测试绿;schema 与本步骤前**逐列一致**(本步不改业务 schema)。
|
||||
All tests green; schema identical to before (no business-schema change).
|
||||
|
||||
---
|
||||
|
||||
## 风险与缓解 / Risks & Mitigations
|
||||
|
||||
- **baseline 与现状有偏差 → `stamp` 失真。** 缓解:`alembic check` 对生产副本校验 + 人眼复核 SQLite 假差异。
|
||||
Baseline drift → `alembic check` against a prod copy + manual eyeball.
|
||||
- **容器内找不到迁移脚本。** 缓解:确认 `Dockerfile` 已 `COPY` `alembic.ini` 与 `migrations/`;`script_location` 用绝对/相对镜像 WORKDIR(`/app`) 正确解析。
|
||||
Migrations missing in image → ensure they're `COPY`-ed and `script_location` resolves under `/app`.
|
||||
|
||||
---
|
||||
|
||||
## 相关约定 / Conventions(详见 implementation-plan.md)
|
||||
|
||||
- 不主动 push/commit,除非业主要求。Don't push/commit unless asked.
|
||||
- 实现与设计若有偏差 → 回写设计文档 §3 与仓库简报 `../repository-brief.md` §10。
|
||||
@@ -0,0 +1,102 @@
|
||||
# 步骤 2 · LLM 接入 / Step 2 · LLM Integration
|
||||
|
||||
> **可独立执行 / Self-contained.** 完整背景见设计文档 [`llm-integration-design.md`](./llm-integration-design.md) §4;跨步骤约定见 [`implementation-plan.md`](./implementation-plan.md)。
|
||||
> **前置 / Prerequisite:** [步骤 1](./step-1-alembic-foundation.md) 已合入(Alembic 已就位——**schema 变更一律通过新建迁移完成**)。Step 1 merged; Alembic is in place — **all schema changes go through a new migration**.
|
||||
> **产出 / Output:** 一个可独立合入的 PR。
|
||||
|
||||
---
|
||||
|
||||
## 目标 / Goal
|
||||
|
||||
提供一个配置页:能填写并测试 OpenAI 兼容的 `base_url`/`model`/`api_key`,配置落库到 `app_settings`;并提供一个可复用、可 mock 的 LLM 客户端。**未配置时整站行为不变。**
|
||||
A settings page to enter & test the LLM config, persisted to `app_settings`, plus a reusable, mockable LLM client. **App behavior is unchanged when unconfigured.**
|
||||
|
||||
---
|
||||
|
||||
## 必要背景 / Essential Context
|
||||
|
||||
- 路由全部在 `app/main.py::create_app()`;模板在 `app/templates/`,基础模板 `base.html` 顶部有导航(现有「箱子」「搜索」两个链接)。
|
||||
All routes live in `create_app()`; templates under `app/templates/`; nav lives in `base.html`.
|
||||
- DB 会话依赖:`Depends(get_db)`(`app/db.py`)。models 在 `app/models.py`,`Base` 在 `app/db.py`。
|
||||
- **同步 handler 即可**:FastAPI 把同步 `def` 路由丢线程池执行,阻塞式 `httpx` 调用可接受。
|
||||
Sync handlers are fine — FastAPI runs them in a threadpool, so blocking `httpx` is acceptable.
|
||||
- `httpx` 已在 `requirements.txt`,**不要新增依赖**(不引入 `openai` SDK)。
|
||||
`httpx` is already a dependency; **add no new deps**.
|
||||
|
||||
### 关键决策 / Key Decisions
|
||||
|
||||
- **配置存储用键值表**,不是定型列:`app_settings(key TEXT PRIMARY KEY, value TEXT)`。原因:后续配置项会变多,KV 加项=加一行、永不迁移;类型/校验在 Python 侧。
|
||||
KV table, not typed columns — future settings = new rows, never a migration.
|
||||
- **API Key 明文落库**(业主在其威胁模型下的明确选择),但**配置页绝不回显明文**:显示「已配置,留空=不修改」,提交留空则保留原值。
|
||||
Plaintext key in DB (owner's explicit choice), but the **UI never echoes it** — show "configured, leave blank to keep".
|
||||
- **优雅降级**:`llm_enabled` 关或缺 `model`/`api_key` 时,`is_configured()` 为假;调用失败不抛 500,返回可识别的失败信号。
|
||||
Graceful degradation throughout.
|
||||
|
||||
### 本轮使用的 key / Keys 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) | (空 / empty) |
|
||||
| `ai_search_enabled` | AI 搜索功能开关(步骤 3 用)/ AI-search toggle | `false` |
|
||||
|
||||
---
|
||||
|
||||
## 任务 / Tasks
|
||||
|
||||
- [ ] **新建 V2 迁移**(用 Alembic,遵循步骤 1 的工作流):创建 `app_settings(key TEXT PRIMARY KEY, value TEXT)`。
|
||||
New V2 Alembic migration creating `app_settings`.
|
||||
- [ ] `app/models.py`:新增 `AppSetting` 模型(映射 `app_settings`)。
|
||||
- [ ] 配置读写 helper(建议放 `app/settings_store.py` 或 `app/config.py` 旁):
|
||||
- `get_app_settings(db) -> LLMConfig`(dataclass:`enabled`/`base_url`/`model`/`api_key`/`ai_search_enabled`,含默认值)。
|
||||
- `save_app_settings(db, ...)`:写回 KV;Key 留空则不覆盖原值。
|
||||
- [ ] 新增 `app/llm.py`(基于 `httpx`):
|
||||
- [ ] `is_configured(cfg) -> bool`
|
||||
- [ ] `test_connection(cfg) -> Result`(发最小请求验证 `base_url`/`model`/`api_key`)。
|
||||
- [ ] `expand_query(cfg, query) -> list[str]`(查询词扩展;**步骤 3 会用**,本步先落地+单测)。
|
||||
- [ ] 统一超时 + 错误处理;失败优雅降级。
|
||||
- [ ] **(预留,不实现)** `analyze_image(...)`:仅留 TODO/签名占位 + 注释指向"未来图片分析轮次"。Reserved, not implemented.
|
||||
- [ ] 把所有网络调用收敛到**单一函数边界**,便于测试整体 mock。
|
||||
- [ ] 路由(`app/main.py`):
|
||||
- [ ] `GET /settings`:渲染配置表单(Key 脱敏)。
|
||||
- [ ] `POST /settings`:保存到 `app_settings`(303 重定向,沿用现有 POST 风格)。
|
||||
- [ ] `POST /settings/test`:用当前/待保存配置测试连接,回显结果。
|
||||
- [ ] 模板:`app/templates/settings/form.html`(沿用现有卡片/表单样式);`base.html` 导航加「设置」入口。
|
||||
- [ ] 测试(LLM 全程 mock,CI 不联网):
|
||||
- [ ] 保存/读取配置;**Key 脱敏**(响应 HTML 不含明文;提交留空不覆盖原 Key)。
|
||||
- [ ] `POST /settings/test` 成功/失败两条分支(mock `test_connection` 或底层 httpx)。
|
||||
- [ ] 未配置时 `is_configured()` 为假;配置页在 `llm_enabled=false` 下仍可正常打开保存。
|
||||
|
||||
---
|
||||
|
||||
## 涉及文件 / Files
|
||||
|
||||
`migrations/versions/**`(V2)、`app/models.py`、`app/llm.py`(新)、`app/settings_store.py`(新,或并入既有模块)、`app/main.py`、`app/templates/settings/form.html`(新)、`app/templates/base.html`、`tests/`。
|
||||
|
||||
---
|
||||
|
||||
## 验收 / Acceptance
|
||||
|
||||
- 在 `/settings` 填入配置 → 保存 → 重启应用后仍在(已落库)。Config persists across restarts.
|
||||
- 「测试连接」对真实 OpenAI 端点可用(手动验证);自动化测试中走 mock。
|
||||
- 配置页 HTML **不含明文 Key**;留空提交保留原值。
|
||||
- `llm_enabled=false` 或缺 Key 时,全站行为与步骤 1 后一致(无回归)。
|
||||
|
||||
---
|
||||
|
||||
## 风险与缓解 / Risks & Mitigations
|
||||
|
||||
- **把网络调用散落各处 → 难 mock、CI 易联网。** 缓解:所有外呼集中在 `app/llm.py` 单一边界。
|
||||
Scattered network calls → keep all egress in `app/llm.py`.
|
||||
- **Key 不慎回显。** 缓解:模板永不输出 `api_key` 值,仅输出"是否已配置"。
|
||||
Accidental key echo → template never prints the key value.
|
||||
|
||||
---
|
||||
|
||||
## 相关约定 / Conventions(详见 implementation-plan.md)
|
||||
|
||||
- 不主动 push/commit,除非业主要求。
|
||||
- 无新依赖(用 `httpx`)。CI 不联网(mock LLM)。
|
||||
- 实现与设计若有偏差 → 回写设计文档 §4 与仓库简报 §15。
|
||||
@@ -0,0 +1,86 @@
|
||||
# 步骤 3 · 基础 AI 搜索 / Step 3 · Basic AI Search
|
||||
|
||||
> **可独立执行 / Self-contained.** 完整背景见设计文档 [`llm-integration-design.md`](./llm-integration-design.md) §5;跨步骤约定见 [`implementation-plan.md`](./implementation-plan.md)。
|
||||
> **前置 / Prerequisite:** [步骤 2](./step-2-llm-integration.md) 已合入(`app/llm.py::expand_query`、`app_settings` 配置、`ai_search_enabled` 开关均已就绪)。Step 2 merged.
|
||||
> **产出 / Output:** 一个可独立合入的 PR;**不改 schema**。
|
||||
|
||||
---
|
||||
|
||||
## 目标 / Goal
|
||||
|
||||
在搜索页提供一个**常驻**的「AI 智能搜索」动作:点击后用查询词扩展增强搜索结果。**不以"零结果"为前提**——即便普通搜索已出结果,用户不满意时也能用。
|
||||
A **persistent** "AI search" action on the search page that broadens results via query-term expansion. **Not gated on zero results** — usable even when normal results exist.
|
||||
|
||||
---
|
||||
|
||||
## 必要背景 / Essential Context
|
||||
|
||||
- 现有搜索:`app/main.py::_build_search_results(db, query)` 对 `Box`/`Item`/`SubItem` 的 `name` 与 `note` 做大小写不敏感 `LIKE`,返回结果列表;路由 `GET /search`(函数 `search_page`,参数 `q`)渲染 `app/templates/search/index.html`。
|
||||
Existing search: `_build_search_results(db, query)` does case-insensitive `LIKE` over name/note; route `GET /search` renders `search/index.html`.
|
||||
- 步骤 2 已提供:`app/llm.py::expand_query(cfg, query) -> list[str]`、配置读取 `get_app_settings(db)`、开关 `ai_search_enabled` 与 `is_configured(cfg)`。
|
||||
- 本轮检索范围=`name` + `note`(`image_description` 本轮不存在,属未来图片分析轮次)。
|
||||
Search scope = `name` + `note` (no `image_description` this round).
|
||||
|
||||
### 关键决策 / Key Decisions
|
||||
|
||||
- **常驻、不依赖零结果。** 普通 `LIKE` 照常先出结果;AI 动作始终可用(开启且已配置时)。
|
||||
Persistent and not gated on zero results.
|
||||
- **流程:** 触发 AI → `expand_query` 得到"原词 + 一批近义/相关词" → 用这组词对 `name`/`note` 做 OR `LIKE` 重搜 → 展示,并用横幅标注「AI 帮你扩展了:…」。**只把查询词发出去**,不外泄物品清单。
|
||||
Trigger → expand → OR-`LIKE` over the original + expanded terms → render with a banner of the expansion. Only the query leaves.
|
||||
- **可替换的检索 seam。** 把 AI 检索抽成一个函数(如 `ai_search(db, query) -> (expanded_terms, results)`),本轮内部=查询词扩展 + 本地 `LIKE`;**未来换成向量嵌入 + 相似度时,路由与模板不变**。
|
||||
Wrap AI retrieval behind a swappable seam so embeddings can replace it later without touching route/template.
|
||||
- **优雅降级。** AI 关闭/未配置 → 不显示按钮(或提示去 `/settings`);调用失败 → 友好提示 + 回退普通结果。
|
||||
|
||||
---
|
||||
|
||||
## 任务 / Tasks
|
||||
|
||||
- [ ] 实现检索 seam:在 `app/main.py`(或抽一个小搜索模块 `app/search.py`)加 `ai_search(db, query) -> (expanded_terms, results)`:
|
||||
- 调 `expand_query(cfg, query)` 得到扩展词;
|
||||
- 用「原词 + 扩展词」对 `name`/`note` 做 OR `LIKE`(**复用现有 `_build_search_results` 的匹配逻辑**,避免重复实现),去重。
|
||||
- 注意:现有 `_build_search_results(db, query)` 只接收单个查询词;建议把它泛化为接收一组关键词(对多个词做 OR),让 AI 搜索与普通搜索共用同一套匹配逻辑,避免分叉。
|
||||
Note: `_build_search_results` currently takes a single query — generalize it to accept multiple keywords so AI and normal search share one matching path.
|
||||
- [ ] 扩展 `GET /search`:支持 `ai=1` 触发位(如 `GET /search?q=锅&ai=1`),保持单页、可收藏、SSR 友好。
|
||||
- `ai=1` 且 AI 开启且 `is_configured()` → 走 `ai_search`,把 `expanded_terms` 传给模板做横幅。
|
||||
- 否则走原有普通搜索。
|
||||
- [ ] 模板 `app/templates/search/index.html`:
|
||||
- 常驻「AI 智能搜索」按钮,链接到 `?q=<当前词>&ai=1`;
|
||||
- AI 关闭/未配置时隐藏按钮(或显示去 `/settings` 的提示);
|
||||
- `ai=1` 结果页顶部显示横幅「AI 帮你扩展了:term1、term2…」。
|
||||
- [ ] 降级:`ai_search` 内部调用失败时捕获,渲染友好提示并回退到普通 `LIKE` 结果。
|
||||
- [ ] 测试(mock `expand_query`,CI 不联网):
|
||||
- [ ] 扩展词驱动命中:原词 `LIKE` 搜不到、扩展后能搜到。
|
||||
- [ ] 已有结果时点 AI 仍可用,且结果集被扩大(含原结果)。
|
||||
- [ ] 按钮可见性随 `ai_search_enabled` + `is_configured()` 门控。
|
||||
- [ ] 调用失败 → 回退普通结果、页面不报错。
|
||||
|
||||
---
|
||||
|
||||
## 涉及文件 / Files
|
||||
|
||||
`app/main.py`、(可选 `app/search.py`)、`app/templates/search/index.html`、`tests/`。
|
||||
|
||||
---
|
||||
|
||||
## 验收 / Acceptance
|
||||
|
||||
- 搜索页在 AI 开启时**始终**可见「AI 智能搜索」;点击后结果按扩展词扩大,并标注扩展词。
|
||||
- 未配置/失败时优雅降级,普通搜索完全不受影响。
|
||||
- 检索逻辑收敛在 `ai_search` seam,未来可整体替换为向量语义搜索而不动路由/模板。
|
||||
|
||||
---
|
||||
|
||||
## 风险与缓解 / Risks & Mitigations
|
||||
|
||||
- **扩展词过多/过散 → 结果噪声大。** 缓解:限制扩展词数量;横幅透明展示扩展词,让用户理解结果来源。
|
||||
Too many/too-loose terms → cap the expansion count and show it transparently.
|
||||
- **AI 调用慢/失败拖累搜索页。** 缓解:仅在 `ai=1` 时才调用(普通搜索零开销);设超时;失败回退。
|
||||
Slow/failed calls → only call on `ai=1`, set a timeout, fall back.
|
||||
|
||||
---
|
||||
|
||||
## 相关约定 / Conventions(详见 implementation-plan.md)
|
||||
|
||||
- 不主动 push/commit,除非业主要求。
|
||||
- CI 不联网(mock `expand_query`)。
|
||||
- 实现与设计若有偏差 → 回写设计文档 §5 与仓库简报 §15。
|
||||
Reference in New Issue
Block a user