feat(secrets-mcp): /entries 按 tags 筛选,发版 0.5.27
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 5m48s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 5s

- Web:tags 查询参数、folder 计数 SQL、分页与 tab 保留 tags;模板与 i18n
- README / AGENTS / CHANGELOG;plans/web-tags-filter 等计划文档
This commit is contained in:
voson
2026-04-11 21:38:26 +08:00
parent 43d6164a15
commit f86d12b80e
11 changed files with 364 additions and 113 deletions

View File

@@ -3,6 +3,7 @@
**日期**: 2026-04-11
**来源**: 两个 AI 实现对比评估
**比较对象**:
- `d7720662` (`/Users/voson/work/refining/secrets-cr-fixes-ws`)
- `9f8a68cd` (`/Users/voson/work/refining/secrets/plan-impl`)
@@ -10,9 +11,10 @@
## 结论
**`d7720662`** 为主线采纳。
`**d7720662`** 为主线采纳。
**原因**:
1. `rollback` 的 live row 加锁与 snapshot 读取都在事务内完成,更符合原计划里对 TOCTOU 的修复要求。
2. Web JSON API 的 session 校验保留了按 `UiLang` 返回错误信息的行为,没有把错误消息退化成固定英文。
3. `svc_add` 返回 `entry_id`MCP 层直接使用返回值建立 parent relation和计划第 5 项更一致。
@@ -35,10 +37,9 @@
仅吸收下面两处,手动改写,不直接整文件 cherry-pick
1. `crates/secrets-mcp/src/web/entries.rs`
- 把长度校验报错文案改成基于 `crate::validation::*` 常量拼接,避免上限数字硬编码在文案里。
- 把长度校验报错文案改成基于 `crate::validation::`* 常量拼接,避免上限数字硬编码在文案里。
2. `crates/secrets-core/src/service/env_map.rs`
-`env_prefix_with_and_without_prefix` 单测。
-`env_prefix_with_and_without_prefix` 单测。
---
@@ -47,18 +48,21 @@
### 不采纳 `9f8a68cd` 的 `rollback.rs`
原因:
- 它仍然先在事务外读取 `entries_history`,再开启事务并锁 live row。
- 对“回滚到最近快照”的路径仍存在先读后锁的时间窗口。
### 不采纳 `9f8a68cd` 的 `web/mod.rs`
原因:
- `load_session_user_strict()` / `require_valid_user_json()` 返回固定英文 JSON 错误。
- 会丢失现有多语言错误语义。
### 不采纳 `9f8a68cd` 的 `AddResult.id`
原因:
- 本轮计划里明确要求 `svc_add` 返回 `entry_id`
- `d7720662` 的字段命名与 MCP 使用方式更贴近计划要求。
@@ -82,32 +86,26 @@
### 尚需补齐的验证
1. export/import round-trip 测试
- `password` / `key` / `text` 三种类型导出再导入后保持不变
- `password` / `key` / `text` 三种类型导出再导入后保持不变
2. legacy import 测试
- 老格式缺失 `secret_types` 时默认回落到 `text`
- 老格式缺失 `secret_types` 时默认回落到 `text`
3. env map 测试
- `db.password` vs `db_password`
-`prefix`
- 多 entry 合并冲突
- `db.password` vs `db_password`
-`prefix`
- 多 entry 合并冲突
4. rollback 测试
- 恢复字段是否符合预期
- 并发更新 + 回滚不依赖过期值
- 恢复字段是否符合预期
- 并发更新 + 回滚不依赖过期值
5. `regenerate_api_key` 测试
- 正常用户返回新 key
- 不存在用户返回错误
- 正常用户返回新 key
- 不存在用户返回错误
6. MCP tool 测试
- `secrets_find` count 失败路径
- `secrets_rollback` 无 encryption key 也可执行
- `secrets_find` count 失败路径
- `secrets_rollback` 无 encryption key 也可执行
7. Web session / validation 测试
- `key_version` mismatch -> `401`
- 用户不存在 / session 损坏 -> 正确错误
- `folder/type/name/notes` 超长 -> `400`
- `key_version` mismatch -> `401`
- 用户不存在 / session 损坏 -> 正确错误
- `folder/type/name/notes` 超长 -> `400`
---
@@ -126,18 +124,18 @@ cargo clippy --locked -- -D warnings
cargo test --locked
```
7. 跑发布前检查:
1. 跑发布前检查:
```bash
./scripts/release-check.sh
```
8. 确认版本和 tag
- `crates/secrets-mcp/Cargo.toml` 已 bump合并执行时为 `0.5.21`,因 `crates/**` 有变更)
- `jj tag list`
1. 确认版本和 tag
- `crates/secrets-mcp/Cargo.toml` 已 bump合并执行时为 `0.5.21`,因 `crates/**` 有变更)
- `jj tag list`
---
## 备注
如果后续要做最终合并,建议以 `d7720662` 为基础继续补测试,而不是尝试把两份实现整合成第三套逻辑。这样改动面最小,风险也最低。
如果后续要做最终合并,建议以 `d7720662` 为基础继续补测试,而不是尝试把两份实现整合成第三套逻辑。这样改动面最小,风险也最低。

View File

@@ -23,7 +23,7 @@ Add a new `metadata_query` filter to `SearchParams` that uses PostgreSQL `jsonb_
#### secrets-core
**`crates/secrets-core/src/service/search.rs`**
`**crates/secrets-core/src/service/search.rs`**
- Add `metadata_query: Option<&'a str>` field to `SearchParams`
- In `entry_where_clause_and_next_idx`, when `metadata_query` is set, add:
@@ -42,7 +42,7 @@ EXISTS (
#### secrets-mcp (MCP tools)
**`crates/secrets-mcp/src/tools.rs`**
`**crates/secrets-mcp/src/tools.rs**`
- Add `metadata_query` field to `FindInput`:
@@ -56,7 +56,7 @@ metadata_query: Option<String>,
#### secrets-mcp (Web)
**`crates/secrets-mcp/src/web/entries.rs`**
`**crates/secrets-mcp/src/web/entries.rs**`
- Add `metadata_query: Option<String>` to `EntriesQuery`
- Thread it into all `SearchParams` usages (count, list, folder counts)
@@ -64,17 +64,19 @@ metadata_query: Option<String>,
- Add `metadata_query` to `EntriesPageTemplate` and filter form hidden fields
- Include `metadata_query` in pagination `href` links
**`crates/secrets-mcp/templates/entries.html`**
`**crates/secrets-mcp/templates/entries.html**`
- Add a "metadata 值" text input to the filter bar (after name, before type)
- Preserve value in the input on re-render
### i18n Keys
| Key | zh | zh-Hant | en |
|-----|-----|---------|-----|
| `filterMetaLabel` | 元数据值 | 元数据值 | Metadata value |
| `filterMetaPlaceholder` | 搜索元数据值 | 搜尋元資料值 | Search metadata values |
| Key | zh | zh-Hant | en |
| ----------------------- | ------ | ------- | ---------------------- |
| `filterMetaLabel` | 元数据值 | 元数据值 | Metadata value |
| `filterMetaPlaceholder` | 搜索元数据值 | 搜尋元資料值 | Search metadata values |
### Performance Notes
@@ -191,81 +193,76 @@ pub async fn get_relations_for_entries(
) -> Result<HashMap<Uuid, Vec<RelationSummary>>>
```
**`crates/secrets-core/src/service/mod.rs`** — add `pub mod relations;`
`**crates/secrets-core/src/service/mod.rs**` — add `pub mod relations;`
**`crates/secrets-core/src/db.rs`** — add entry_relations table creation in `migrate()`
`**crates/secrets-core/src/db.rs**` — add entry_relations table creation in `migrate()`
**`crates/secrets-core/src/error.rs`** — no new error variant needed; use `AppError::Validation { message }` for cycle detection and permission errors
`**crates/secrets-core/src/error.rs**` — no new error variant needed; use `AppError::Validation { message }` for cycle detection and permission errors
### MCP Tool Changes
**`crates/secrets-mcp/src/tools.rs`**
`**crates/secrets-mcp/src/tools.rs**`
1. **`secrets_add`** (`AddInput`): add optional `parent_ids: Option<Vec<String>>` field
- Description: "UUIDs of parent entries to link. Creates parent→child relations."
- After creating the entry, call `relations::add_relation` for each parent
2. **`secrets_update`** (`UpdateInput`): add two fields:
- `add_parent_ids: Option<Vec<String>>` — "UUIDs of parent entries to link"
- `remove_parent_ids: Option<Vec<String>>` — "UUIDs of parent entries to unlink"
3. **`secrets_find`** and `secrets_search` output: add `parents` and `children` arrays to each entry result:
```json
1. `**secrets_add**` (`AddInput`): add optional `parent_ids: Option<Vec<String>>` field
- Description: "UUIDs of parent entries to link. Creates parent→child relations."
- After creating the entry, call `relations::add_relation` for each parent
2. `**secrets_update**` (`UpdateInput`): add two fields:
- `add_parent_ids: Option<Vec<String>>` — "UUIDs of parent entries to link"
- `remove_parent_ids: Option<Vec<String>>` — "UUIDs of parent entries to unlink"
3. `**secrets_find**` and `secrets_search` output: add `parents` and `children` arrays to each entry result:
```json
{
"id": "...",
"name": "...",
"parents": [{"id": "...", "name": "...", "folder": "...", "type": "..."}],
"children": [{"id": "...", "name": "...", "folder": "...", "type": "..."}]
}
```
- Fetch relations for all returned entry IDs in a single batch query
```
- Fetch relations for all returned entry IDs in a single batch query
### Web Changes
**`crates/secrets-mcp/src/web/entries.rs`**
`**crates/secrets-mcp/src/web/entries.rs**`
1. **New API endpoints:**
- `POST /api/entries/{id}/relations` — add parent relation
- Body: `{ "parent_id": "uuid" }`
- Validates same-user ownership and cycle detection
- `DELETE /api/entries/{id}/relations/{parent_id}` — remove parent relation
- `GET /api/entries/options?q=xxx` — lightweight search for parent selection modal
- Returns `[{ "id": "...", "name": "...", "folder": "...", "type": "..." }]`
- Used by the edit dialog's parent selection autocomplete
- `POST /api/entries/{id}/relations` — add parent relation
- Body: `{ "parent_id": "uuid" }`
- Validates same-user ownership and cycle detection
- `DELETE /api/entries/{id}/relations/{parent_id}` — remove parent relation
- `GET /api/entries/options?q=xxx` — lightweight search for parent selection modal
- Returns `[{ "id": "...", "name": "...", "folder": "...", "type": "..." }]`
- Used by the edit dialog's parent selection autocomplete
2. **Entry list template data** — include parent/child counts per entry row
3. `**api_entry_patch`** — extend `EntryPatchBody` with optional `parent_ids: Option<Vec<Uuid>>`
- When present, replace all parent relations for this entry with the given list
- This is simpler than incremental add/remove in the Web UI context
3. **`api_entry_patch`** — extend `EntryPatchBody` with optional `parent_ids: Option<Vec<Uuid>>`
- When present, replace all parent relations for this entry with the given list
- This is simpler than incremental add/remove in the Web UI context
**`crates/secrets-mcp/templates/entries.html`**
`**crates/secrets-mcp/templates/entries.html**`
1. **List table**: add a "关联" (relations) column showing parent/child counts as clickable chips
2. **Edit dialog**: add "上级条目" (parent entries) section
- Show current parents as removable chips
- Add a search-as-you-type input that queries `/api/entries/options`
- Click a search result to add it as parent
- On save, send `parent_ids` in the PATCH body
- Show current parents as removable chips
- Add a search-as-you-type input that queries `/api/entries/options`
- Click a search result to add it as parent
- On save, send `parent_ids` in the PATCH body
3. **View dialog / detail**: show "下级条目" (children) list with clickable links that navigate to the child entry
4. **i18n**: add keys for all new UI elements
### i18n Keys (Entry Relations)
| Key | zh | zh-Hant | en |
|-----|-----|---------|-----|
| `colRelations` | 关联 | 關聯 | Relations |
| `parentEntriesLabel` | 上级条目 | 上級條目 | Parent entries |
| `childrenEntriesLabel` | 级条目 | 級條目 | Child entries |
| `addParentLabel` | 添加上级 | 新增上級 | Add parent |
| `removeParentLabel` | 移除上级 | 移除上級 | Remove parent |
| `searchEntriesPlaceholder` | 搜索条目… | 搜尋條目… | Search entries… |
| `noParents` | 无上级 | 無上級 | No parents |
| `noChildren` | 无级 | 無| No children |
| `relationCycleError` | 无法添加:会形成循环引用 | 無法新增:會形成循環引用 | Cannot add: would create a cycle |
| Key | zh | zh-Hant | en |
| -------------------------- | ------------ | ------------ | -------------------------------- |
| `colRelations` | 关联 | 關聯 | Relations |
| `parentEntriesLabel` | 级条目 | 級條目 | Parent entries |
| `childrenEntriesLabel` | 下级条目 | 下級條目 | Child entries |
| `addParentLabel` | 添加上级 | 新增上級 | Add parent |
| `removeParentLabel` | 移除上级 | 移除上級 | Remove parent |
| `searchEntriesPlaceholder` | 搜索条目… | 搜尋條目… | Search entries… |
| `noParents` | 无 | 無 | No parents |
| `noChildren` | 无下级 | 無下級 | No children |
| `relationCycleError` | 无法添加:会形成循环引用 | 無法新增:會形成循環引用 | Cannot add: would create a cycle |
### Audit Logging
@@ -276,7 +273,7 @@ Log relation changes in the existing `audit::log_tx` system:
### Export / Import
**`ExportEntry`** — add optional `parents: Vec<ParentRef>` where:
`**ExportEntry`** — add optional `parents: Vec<ParentRef>` where:
```rust
pub struct ParentRef {
@@ -368,25 +365,28 @@ This is idempotent (uses `IF NOT EXISTS`) and will run automatically on next sta
## Testing Checklist
### Metadata Search
- [ ] `metadata_query=1.2.3.4` matches entries where any metadata value contains "1.2.3.4"
- [ ] `metadata_query=1.2.3.4` does NOT match entries where only the key contains "1.2.3.4"
- [ ] `metadata_query` works with nested metadata (e.g. `{"server": {"ip": "1.2.3.4"}}`)
- [ ] `metadata_query` combined with `folder`/`type`/`tags` filters works correctly
- [ ] `metadata_query` with special characters (`%`, `_`) is properly escaped
- [ ] Existing `query` parameter behavior is unchanged
- [ ] Web filter bar preserves `metadata_query` across pagination and folder tab clicks
- `metadata_query=1.2.3.4` matches entries where any metadata value contains "1.2.3.4"
- `metadata_query=1.2.3.4` does NOT match entries where only the key contains "1.2.3.4"
- `metadata_query` works with nested metadata (e.g. `{"server": {"ip": "1.2.3.4"}}`)
- `metadata_query` combined with `folder`/`type`/`tags` filters works correctly
- `metadata_query` with special characters (`%`, `_`) is properly escaped
- Existing `query` parameter behavior is unchanged
- Web filter bar preserves `metadata_query` across pagination and folder tab clicks
### Entry Relations
- [ ] Can add a parent→child relation between two entries
- [ ] Can add multiple parents to a single entry
- [ ] Cannot add self-referencing relation (CHECK constraint)
- [ ] Cannot create a direct cycle (A→B→A)
- [ ] Cannot create an indirect cycle (A→B→C→A)
- [ ] Cannot link entries from different users
- [ ] Deleting an entry removes all its relation edges but leaves related entries intact
- [ ] MCP `secrets_add` with `parent_ids` creates relations
- [ ] MCP `secrets_update` with `add_parent_ids`/`remove_parent_ids` modifies relations
- [ ] MCP `secrets_find`/`secrets_search` output includes `parents` and `children`
- [ ] Web entry list shows relation counts
- [ ] Web edit dialog allows adding/removing parents
- [ ] Web entry view shows children with navigation links
- Can add a parent→child relation between two entries
- Can add multiple parents to a single entry
- Cannot add self-referencing relation (CHECK constraint)
- Cannot create a direct cycle (A→B→A)
- Cannot create an indirect cycle (A→B→C→A)
- Cannot link entries from different users
- Deleting an entry removes all its relation edges but leaves related entries intact
- MCP `secrets_add` with `parent_ids` creates relations
- MCP `secrets_update` with `add_parent_ids`/`remove_parent_ids` modifies relations
- MCP `secrets_find`/`secrets_search` output includes `parents` and `children`
- Web entry list shows relation counts
- Web edit dialog allows adding/removing parents
- Web entry view shows children with navigation links

View File

@@ -20,6 +20,7 @@
### 2. 查看密文弹窗 — 增加管理功能
在每个解密字段行中增加:
- **重命名输入框**inline edit带 debounce 校验)
- **类型下拉选择**
- **解绑按钮**
@@ -51,4 +52,4 @@
## 涉及文件
- `crates/secrets-mcp/templates/entries.html`HTML + JS + CSS
- `crates/secrets-mcp/src/web/entries.rs`(无需修改,复用现有 API
- `crates/secrets-mcp/src/web/entries.rs`(无需修改,复用现有 API

178
plans/web-tags-filter.md Normal file
View File

@@ -0,0 +1,178 @@
# Web 条目页 tags 筛选计划
## 目标
在 Web `/entries` 页面补齐 `tags` 筛选能力,使现有 `tags` 字段在 Web、MCP、数据层三者之间保持一致。
本次只做最小实现:支持用户在 Web 上输入 tags 条件并筛选条目,不改动数据库结构,不新增 MCP tool 参数,不改动条目编辑语义。
## 当前状态
- 数据层已支持 `tags` 过滤:`crates/secrets-core/src/service/search.rs`
- MCP 已支持 `tags` 参数:`crates/secrets-mcp/src/tools.rs`
- Web `/entries` 仅展示 tags 列与编辑字段,没有筛选入口:
- 查询参数缺少 `tags``crates/secrets-mcp/src/web/entries.rs`
- 模板筛选栏缺少 tags 输入:`crates/secrets-mcp/templates/entries.html`
- Web 查询当前固定传 `tags: &[]`
## 范围
### 包含
- `/entries` 页面增加 tags 筛选输入
- 后端将 tags 解析并传入 `SearchParams`
- 分页、folder tabs、筛选重置后的 URL 状态保持一致
- i18n 文案补齐
### 不包含
- MCP 工具改造
- 数据库迁移或索引变更
- `/trash` 页面筛选增强
- 新增 tags 自动补全、标签选择器、标签管理页
## 交互定义
### 输入方式
-`/entries` 筛选栏增加一个 `tags` 文本输入框
- 输入格式采用逗号分隔,例如:`prod, aliyun`
- 服务端按逗号拆分、`trim`、去掉空字符串
### 匹配语义
- 继续复用现有搜索层语义:`tags @> ARRAY[...]::text[]`
- 即:用户输入多个 tags 时,要求条目同时包含这些 tagsAND 语义)
### 状态保持
- 筛选提交后,输入框保留原值
- 分页上一页/下一页链接保留 `tags`
- folder tabs 切换时保留 `tags`
- `重置` 仍回到 `/entries`,清空所有筛选条件
## 实施步骤
### 1. 扩展 Web 查询参数与模板上下文
文件:`crates/secrets-mcp/src/web/entries.rs`
-`EntriesQuery` 中增加 `tags: Option<String>`
-`EntriesPageTemplate` 中增加 `filter_tags: String`
-`entries_page` 中读取原始 tags 字符串,用于模板回填
- 将原始字符串解析为 `Vec<String>`,供 `SearchParams.tags` 使用
建议新增一个局部辅助函数,职责仅限于:
- 接收 `Option<&str>`
- 按逗号分割
- `trim`
- 过滤空值
- 返回 `Vec<String>`
保持逻辑局部化,避免把 tags 解析散落到多个位置。
### 2. 将 tags 传入条目查询与计数
文件:`crates/secrets-mcp/src/web/entries.rs`
- 更新 `count_params`,不再使用 `tags: &[]`
- 更新 `list_params`,复用相同 tags 切片
- 确保总数统计、分页列表与实际筛选条件一致
## 3. 让 folder tabs 计数遵循相同 tags 条件
文件:`crates/secrets-mcp/src/web/entries.rs`
当前 folder tabs 使用手写 SQL 统计各 folder 数量,需要同步加入 tags 条件,否则会出现:
- 列表已按 tags 过滤
- 但 folder tab 数量仍是未过滤结果
实现方式:
- 在构建 `folder_sql` 时,当 tags 非空,追加 `tags @> ARRAY[...]::text[]`
- 对应补齐 bind 参数
- 保持与 `SearchParams` 的过滤语义完全一致
## 4. 让 URL 生成函数保留 tags
文件:`crates/secrets-mcp/src/web/entries.rs`
- 扩展 `entries_href(...)` 参数,加入 `tags: Option<&str>`
- 在 folder tabs 链接中传入当前 tags
- 在需要保留筛选状态的地方一并传递 tags
## 5. 更新模板筛选栏与分页链接
文件:`crates/secrets-mcp/templates/entries.html`
- 在筛选表单中新增 tags 输入框
- 输入框 value 绑定 `filter_tags`
- 为 tags 输入框增加 i18n label / placeholder
- 分页链接 `上一页/下一页` 补充 `tags` query 参数
建议放置位置:名称与元数据值之间或元数据值与类型之间,保持现有筛选栏布局最小改动。
## 6. 补齐前端文案
文件:`crates/secrets-mcp/templates/entries.html`
新增 i18n key
- `filterTagsLabel`
- `filterTagsPlaceholder`
建议文案:
- zh-CN: `标签` / `多个标签用逗号分隔`
- zh-Hant: `標籤` / `多個標籤用逗號分隔`
- en: `Tags` / `Comma-separated tags`
## 验收标准
### 功能验收
- 访问 `/entries?tags=prod` 时,只返回包含 `prod` 的条目
- 访问 `/entries?tags=prod,aliyun` 时,只返回同时包含 `prod``aliyun` 的条目
- tags 两侧空格不影响结果,例如 `prod, aliyun`
- 空字符串、重复逗号不会报错,例如 `prod,,aliyun`
- 分页后 `tags` 不丢失
- 切换 folder tab 后 `tags` 不丢失
- 重置后清空 `tags`
### 一致性验收
- 页面总数、列表内容、folder tab 数量使用同一组 tags 条件
- Web 语义与 MCP / `SearchParams` 语义一致,均为 AND 匹配
## 风险与注意点
- folder tabs 计数 SQL 是手写的,最容易漏掉 tags 绑定顺序
- `list_params` 基于 `count_params` 结构展开,注意借用生命周期不要引入临时值悬垂
- 分页链接和 `entries_href` 若漏传 `tags`,用户会感觉筛选“偶尔失效”
- 现阶段不做 tags 规范化;输入 `Prod` 与存储 `prod` 是否匹配,取决于数组元素本身是否完全一致
## 可选后续
如果上线后确认 `tags` 仍被频繁使用,可以继续做:
1. tags chip UI而不是纯文本输入
2. 常用 tags 自动补全
3. 在 Web 过滤栏里明确提示“多个标签为同时匹配”
4. 评估是否需要大小写规范化策略
## 验证建议
实现后至少执行:
```bash
cargo fmt -- --check
cargo test --locked
```
如果本次提交涉及 `crates/**`,按仓库规则在提交前再执行:
```bash
./scripts/release-check.sh
```