1138 Commits

Author SHA1 Message Date
xfy
cc88a7e90a fix(mcp): 修正 Oh-My-Pi 配置格式与路径
三处错误(据 omp 客户端实际报错):
1. 字段名 transport→type,值 streamable-http→http:omp 不识别 transport,
   遇未知字段退化为 stdio 因缺 command 报错丢弃
2. 路径 ~/.pi/agent/mcp.json→.mcp.json / ~/.omp/agent/mcp.json / ~/.mcp.json
3. omp 与 Claude Code 的 JSON 体现完全相同,差异仅在配置文件路径
2026-07-28 16:39:43 +08:00
xfy
3f3bf411ca feat(mcp): 添加 OpenCode 客户端配置(第二位)
OpenCode 用 opencode.json(非 mcp.json),根键 mcp(非 mcpServers),
远程端点 type: remote(非 streamable-http),带 $schema 与 enabled 字段。
来源:2026 opencode.ai 官方文档。同步更新骨架屏形状数组(0..7)。
2026-07-28 16:34:15 +08:00
xfy
9d7f01a4ce feat(mcp): 令牌列表添加骨架屏
TokenList 首次加载时(loading 且无数据)用 DelayedSkeleton 包裹的
骨架表格占位,4 行 7 列对齐真实表格列宽;加载完成后才区分空态/表格。
修复了首次加载时误显「暂无令牌」的问题。
2026-07-28 16:19:36 +08:00
xfy
2d995d6eaa refactor(mcp): Oh-My-Pi 配置移至首位
将 Oh-My-Pi 客户端配置排在第一,同步调整骨架屏形状数组顺序。
2026-07-28 16:15:23 +08:00
xfy
ee1732214e feat(mcp): 配置生成中 loading 替换为骨架屏
用 SkeletonBox 组合出 6 个配置片段的占位结构(标题行 + 代码块),
高度按真实 JSON/CLI 配置长短变化,比纯文本「生成配置中…」更直观。
2026-07-28 16:11:42 +08:00
xfy
070d06dacd fix(mcp): 复制按钮添加就地反馈
Toast 渲染在页面顶部,配置区在页面最末,点击复制时 Toast 落在视口外看不到。
为每个 ConfigSnippet 复制按钮加本地 copied 信号:点击后按钮就地短暂变绿
提示「已复制」1.5s(经 sleep_ms),不依赖滚动位置。
2026-07-28 16:05:09 +08:00
xfy
1b772f4534 feat(mcp): 客户端配置代码块添加语法高亮
将配置片段 DTO 从扁平字段重构为 snippets Vec(含 content/content_html),
高亮在服务端经 highlight_code 完成(JSON 配置用 json 语法,CLI 用 bash),
前端以 dangerous_inner_html 渲染并包 .md-content 匹配 highlight.css 作用域。

补 json/bash 语法解析测试,确认两者均产出彩色 span 而非纯文本回退。
2026-07-28 15:53:20 +08:00
xfy
53376a0373 fix(mcp): 修正 Claude Code / Cursor 客户端配置格式(联网核实 2026)
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
依据 2026 官方文档联网核实所有 MCP 客户端配置格式,发现两处错误:

1. Claude Code .mcp.json:type 值应为 "http",不是 "streamable-http"。
   Claude Code 用 streamable-http 会静默失败/卡在 connecting 状态。
2. Cursor ~/.cursor/mcp.json:不应带 type 字段——Cursor 按远程 URL 自动
   识别 streamable-http,仅需 url + headers。此前 cursor_json 直接
   clone claude_code_json,继承了错误的 type 字段。

其余配置核实无误:Cline(streamableHttp 驼峰 + disabled/autoApprove)、
Claude Code CLI(--transport http --header)、Oh-My-Pi(transport 字段)。

改动:
- 拆分 cursor_json,不再 clone claude_code_json
- 更新对应 doc 注释(config.rs / mcp_tokens.rs DTO)
- claude_code 测试断言改为 type=="http"
- cursor_json_equals_claude_code_json → cursor_json_has_no_type_field
2026-07-28 15:10:01 +08:00
xfy
78fb51a310 feat(mcp): 客户端配置新增 Oh-My-Pi 片段
在 /admin/mcp 客户端配置展示区增加 Oh-My-Pi(omp)的 MCP 配置片段,
位于 Cline 与「通用」之间。

Oh-My-Pi 的 mcpServers 条目与 Claude Code/Cursor 的关键差异:字段名
是 transport(而非 type),值仍为 streamable-http,auth 同样走
headers.Authorization。配置文件落在 ~/.pi/agent/mcp.json(全局)或
.pi/mcp.json(项目级)。

改动:
- ClientConfigs / McpClientConfigs 新增 omp_json 字段
- generate_client_configs 生成 omp 片段
- ConfigCard 增加 ConfigSnippet 行
- 新增测试 omp_json_uses_transport_field_not_type,并纳入 pretty 校验循环

格式依据:2026 pi.dev / oh-my-pi 官方文档(联网核实)。
2026-07-28 15:04:33 +08:00
xfy
063ad6e606 fix(mcp): 修复 ConfigCard use_effect 死循环
use_effect 内读 loading() 作为重入 guard,使 loading 成为响应式依赖:
effect 设 loading=true → loading 变化触发 effect 重跑 → 早退;
fetch 完成设 loading=false → loading 变化再次触发 effect → 再次跑,
无限循环,导致 /api/get_mcp_client_configs 被疯狂调用。

use_effect 本身按依赖去重,config_token 不变就不会重跑,guard 多余;
删掉 effect 内的 loading() 读取,loading 不再是依赖,循环解除。
(on_manual_input 内的 loading 读取在事件闭包里,不构成响应式订阅,保留。)
2026-07-28 14:57:40 +08:00
xfy
1d1b32cdd2 fix(admin): FormSelect 面板水平居中于窄触发器,修复双倍位移
紧凑触发器(如「手动」73px)下面板仅左对齐,宽出部分全堆在右侧,
视觉失衡。面板改为 left-1/2 + translateX(-50%) 以触发器中心居中,
宽出部分两侧对称探出。

两个 CSS 陷阱及其解法:
- select-enter 关键帧的 transform 会覆盖静态 transform,fill 结束时
  位置跳变 → translateX(-50%) 烘进关键帧首尾(同 popover-enter 先例)
- Tailwind v4 的 -translate-x-1/2 utility 走独立 translate 属性,与
  关键帧的 transform 属性叠加成双倍位移 → 面板改用任意属性类
  [transform:translateX(-50%)]

浏览器实测:紧凑面板中心偏差 -0.13px、胶囊面板 0.00px(动画播完后测量)。
2026-07-28 14:53:59 +08:00
xfy
60360f6b49 fix(mcp): reveal/revoke 令牌时将字符串 id 解析为 Uuid
reveal_mcp_token / revoke_mcp_token 把前端传来的 String id 直接绑到
$1::uuid,tokio-postgres 解析参数类型为 uuid 后拒绝 String,报
'cannot convert between the Rust type String and the Postgres type uuid'
导致 /api/reveal_mcp_token 500。

参照 assets/delete.rs 的做法,绑定前用 uuid::Uuid::parse_str 转换;
解析失败按令牌不存在处理(reveal 返回 None、revoke 静默无操作),
与函数既有的'避免探测'语义一致。
2026-07-28 14:42:14 +08:00
xfy
e6f7704367 feat(admin): 全项目原生 <select> 统一替换为 FormSelect
将剩余 4 处原生下拉(write 编辑器状态胶囊、db_status/server_status
自动刷新、export 导出格式)全部迁移到主题化的 FormSelect:

- FormSelect 新增 trigger_class 可选 prop(缺省全宽表单款),并导出
  FORM_SELECT_COMPACT_CLASS 供工具栏内联小下拉复用;write.rs 状态选择器
  以胶囊 trigger_class 复刻原外观(面板贴底栏自动上翻,替代原
  「隐形 select 覆盖 span」hack)
- 面板宽度改为 max(触发器, 最长选项)(w-max min-w-full + 100vw 上限),
  紧凑触发器(如「手动」)下长选项(如「500ms」「SQL (INSERT)」)
  不再被截断
- db_status/server_status 的字符串 match 反查收编为类型安全选项表
  (Option<u32> 直进直出)
- 因 system/write 页面 SSR 真实引用 FormSelect,移除 FORM_SELECT_ID /
  wrap_index 上不再需要的 #[allow(dead_code)]

src 下已无任何原生 <select> 元素。浏览器实测:紧凑面板 93.8px 宽于
触发器 73.8px、胶囊面板上翻贴顶 6px 间隙、选中 check 与高亮行正确。
2026-07-28 14:38:12 +08:00
xfy
717d17d5a7 feat(admin): FormSelect 自定义主题化下拉弹层,替代原生 select 弹窗
原生 <select> 的弹出列表由 OS/浏览器渲染,暗色主题下是白底系统菜单,
无法跟随 Catppuccin 主题。将 FormSelect 内部重写为
button[aria-haspopup=listbox] + 绝对定位面板(组件签名不变,调用方零改动):

- 面板/选项全部使用语义色 token,与 POPOVER_PANEL_CLASS 同源
  (卡片化圆角 + 阴影),新增 select-enter 入场动画(150ms,
  含 prefers-reduced-motion 豁免)
- 键盘:focus 留在触发器,aria-activedescendant 高亮;↑↓ 循环
  (wrap_index)、Enter/Space 选中、Esc 关闭、Home/End 跳首尾、
  Tab 关闭并自然流转焦点;选项 onmousedown 阻止默认行为避免抢夺焦点
- 透明遮罩拦截外部点击关闭(同 Popover 模式)
- 打开时按视口空间决定展开方向(should_flip + measure_flip,
  下翻/上翻分别设 transform-origin)
- 打开/键盘导航时高亮项 scrollIntoView(block: nearest)
- 选中行:text-paper-accent + check 图标;高亮行:accent-soft 背景

should_flip / wrap_index 抽为纯函数并配单测(边界:恰好放得下、
上方更窄不翻、空列表回绕安全)。
2026-07-28 14:19:52 +08:00
xfy
8b4a2b674e feat(admin): 封装 FormSelect 组件并修复 MCP 页下拉箭头错位
/admin/mcp 的两个 <select> 直接套用文本框的 INPUT_CLASS,原生下拉箭头
由浏览器绘制,深色主题下位置/颜色不协调(垂直不居中、无法跟随主题色)。

- components/forms.rs 新增泛型 FormSelect:appearance-none 隐藏原生箭头,
  右侧叠 pointer-events-none 的 chevron SVG(top-1/2 -translate-y-1/2
  精确居中);option value 内部用序号占位,onchange 直接回传选中项的 T,
  调用方免去「字符串反查枚举」样板
- mcp.rs 两个下拉改用以简形式传入 SCOPE_OPTIONS / LIFETIME_OPTIONS

浏览器实测:chevron 垂直居中偏差 0.00px,右间距 16px(与左内边距对称)。
2026-07-28 13:52:28 +08:00
xfy
ef4c4ee8ed docs(mcp): CHANGELOG + .env.example + AGENTS.md + DEPLOYMENT(T7)
- CHANGELOG:[Unreleased]/Added 记录 MCP 服务器功能(传输/认证/作用域/工具面/加固/配置/依赖/环境变量)。
- .env.example:新增 MCP_TOKEN_ENC_KEY(必填,openssl rand -hex 32)+ RATE_LIMIT_MCP_PER_SEC/_BURST。
- AGENTS.md:新增「MCP Server」章节(传输/认证/令牌存储/作用域/工具组合模式/feature 门控/配置生成)+ Key Directories 加 src/mcp/ + 迁移数 14→17。
- DEPLOYMENT.md:必设变量表加 MCP_TOKEN_ENC_KEY;新增「/mcp 端点」反代说明(catch-all 代理/Origin 白名单取自 APP_BASE_URL/bearer 不经 cookie/独立限流桶)。

实时客户端冒烟测试延后到用户环境(需运行中的 DB + MCP_TOKEN_ENC_KEY + 真实 MCP 客户端);
机制已由 T1 探针验证,集成路径在工单 T7 记录了用户侧步骤。
2026-07-28 12:01:23 +08:00
xfy
45356c3766 feat(mcp): 端点加固 — token 限流、last_used_at 节流、审计日志(T6)
- token-keyed 限流:MCP_LIMITER 按 token_id 计数(与 web 的 IP-keyed 限流隔离),
  默认 10/s burst 30,经 RATE_LIMIT_MCP_PER_SEC/_BURST 可调,超限返回 429。
- last_used_at 节流:仅在 NULL 或距上次 ≥ 60s 时写库,避免高频请求每次 UPDATE。
  resolve_principal 查询带出 last_used_at 做窗口判断。
- 审计日志:鉴权中间件记录每次已认证 MCP 请求(user_id/scope/token_id),
  为自主 AI 客户端的活动留下事后追溯痕迹。
- 输出清理:经设计保证——JSON 序列化转义控制字符,搜索结果硬上限 50,
  rmcp 默认 4MiB 请求体上限兜底。间接提示注入靠作用域隔离 + 客户端人工审批。

验证:clippy --all-features -D warnings 干净;659 单测 + 1 集成全绿;web/server 双目标编译通过。
2026-07-28 11:57:00 +08:00
xfy
9eac025b3e feat(mcp): 完整工具面 + token 管理 + 管理页(T2-T5 并行实现 + 合并)
T2 token 管理(src/api/mcp_tokens.rs):create/list/reveal/revoke + get_mcp_client_configs
服务端函数,均经 get_current_admin_user 鉴权;TokenLifetime 枚举(1/7/30/90天/永不过期)。
src/mcp/config.rs 生成 4 种客户端配置(Claude Code/Cursor/Cline/通用 + CLI)。
src/pages/admin/mcp.rs 令牌管理页:列表 + 新建表单 + 一次性明文展示/重查 + 撤销 +
配置片段卡片。路由 /admin/mcp,导航项加入 admin_layout。

T3 read 工具 + Resources(tools/read.rs, resources.rs):search_posts/get_post/list_tags
(read 作用域,仅已发布);published-post Resources(post://{slug},游标分页)。
search_published 提取为共享函数。

T4 write 工具(tools/{posts,comments,tags,media}.rs):create/update/publish/trash/delete_post、
评论审核、标签 CRUD、媒体 base64 上传(WebP 转码 + 去重)。复用既有 helper 的 SQL 与
缓存失效(moka + ssr_cache)。write 作用域,可读草稿。

T5 admin 工具(tools/{settings,runner}.rs):get/update_settings、run_code(沙箱执行)。
admin 作用域。

合并(server.rs):用 rmcp tool_router 的「命名路由 + 组合」模式,7 个工具组在
YggMcpServer 上 impl,combined_router 用 + 合并成单一 ServerHandler。各工具组鉴权
独立,经 Extension<Parts> 读 McpPrincipal + scope.grants 校验。

新增依赖 base64 0.22(媒体上传解码)。验证:server/web 双目标编译通过、clippy
--all-features -D warnings 干净、659 单测 + 1 集成全绿(+20 新测试)。
2026-07-28 11:53:32 +08:00
xfy
7b9dfa7851 feat(mcp): tracer bullet — /mcp 端点骨架与 search_posts 工具
T1 探针式实现,接通端到端链路并验证双目标编译:

- 依赖:rmcp=3.0.0-beta.3(server/macros/transport-streamable-http-server/
  transport-worker)+ aes-gcm,均经 server feature 门控。
- 迁移 017_mcp_tokens:UUID 主键、scope 约束、token_enc 密文 + token_hash
  唯一索引(仅未撤销),users.id 外键级联。
- 模型 src/models/mcp_token.rs:TokenScope(read<write<admin 偏序,grants)+
  McpToken/McpTokenSummary/CreateTokenResponse DTO。
- src/mcp/(server-only):
  - crypto.rs:AES-GCM-256 加解密(hex 密钥、每次独立 nonce、8 项单测)。
  - auth.rs:bearer→SHA-256→DB 常量查找→注入 McpPrincipal,刷新 last_used_at。
  - server.rs:rmcp ServerHandler,search_posts 工具(read 作用域,pg_trgm 查询)。
  - router.rs:StreamableHttpService(无状态+JSON 响应+Origin 白名单)挂载 /mcp。
- main.rs:merge mcp_route 到应用路由;models/mod.rs 注册 mcp_token 模块。

验证:rmcp+axum-0.8 兼容性经临时探针实测(tools/list、tools/call、Origin→403、
bearer→工具 principal 全链路);--features web 与 --features server 均编译通过;
clippy --all-features -D warnings 干净;639 单测+1 集成 +13 新 MCP 测试全绿。
2026-07-28 11:09:55 +08:00
xfy
801cb79e0d docs(mcp): 新增 MCP 服务器调研与规格
调研(docs/mcp-research.md):对照 2025-11-25 spec 与 rmcp 源码,确认
Streamable HTTP 是 2026 唯一远程传输、stateless 为默认、bearer token 为
单租户务实方案、官方 rmcp 的 Tower StreamableHttpService 可挂载于 axum。

规格(docs/mcp-spec.md):锁定 9 项设计决策(受众/作用域/有效期/检索机制/
工具面/草稿可见性/加密存储/配置目标/构建路径)、模块划分、迁移、验收标准。
rmcp 锁定 =3.0.0-beta.3(稳定版 0.2.1 缺 Origin 校验等 spec 强制项)。
2026-07-28 11:09:45 +08:00
xfy
f6e1dff600 perf(admin): 放慢仪表盘动画节奏
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
- 卡片入场 400→600ms(内联 animation-duration 覆盖),阶梯延迟 0/60/120/180→0/120/240/360ms
- 近期文章行 row-enter 300→450ms,stagger 步进 50→80ms
- CountUp 滚动时长 500→900ms
2026-07-27 14:56:00 +08:00
xfy
61634882c5 feat(admin): 仪表盘添加分块进场动画与数字滚动
- StatCard 三张卡 animate-page-enter 阶梯入场(0/60/120ms),待审评论卡数据就绪后补挂入场类(180ms),近期文章行 animate-row-enter 逐行 stagger(50ms 步进)——动画挂在数据就绪的真实分支上,避免骨架屏截断
- 新增 CountUp 组件:数值 easeOutQuint 滚动(500ms),use_effect 内驱动保持渲染纯净;命中 prefers-reduced-motion 直接显示终值
- input.css 新增 row-enter 关键帧;reduced-motion 媒体查询补全 animate-page-enter/popover-enter/row-enter 覆盖(此前仅 page-enter 生效)
- BTN_SECONDARY 补 active:scale-[0.98] 按压反馈,与 BTN_PRIMARY 对齐
2026-07-27 14:39:10 +08:00
xfy
5743c7a581 feat(upload): 上传图片按内容 SHA-256 去重,重复上传复用已登记素材
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
assets 新增 content_hash 列(唯一索引,迁移 016;存量行保持 NULL
不参与去重)。上传在安全性校验后、转码前对原始字节算 SHA-256:
命中即刷新 created_at/updated_at(重启 7 天清理保护窗)并返回
已有 URL,跳过 GIF/WebP 验证、转码与落盘。并发同内容上传由唯一
索引 + INSERT ... ON CONFLICT DO NOTHING 兜底,落败者删落盘文件
复用胜出者路径。命中响应带 reused: true。仅精确去重,视觉相似
检测(pHash)有意不做。
2026-07-27 13:25:26 +08:00
xfy
775c58a9e6 feat(admin): 素材页支持多选与批量删除
卡片勾选框:仅未引用素材可选(与单删保护语义一致),hover 显现、
有选择时常显,选中卡片加主题色 ring;stop_propagation 防触发灯箱。
批量条:已选计数 / 全选本页(可切换取消本页) / 清除 / 删除所选
(两步确认),选择跨翻页与筛选保留。

服务端新增 BatchDeleteAssets:一次查询取引用存在性,被引用的跳过,
其余删文件(NotFound 容忍)+ 批量删 DB 行 + 清派生缓存,返回
删除/跳过/释放/失败统计。
2026-07-27 11:40:02 +08:00
xfy
248060d920 feat(admin): 素材页搜索框添加 300ms 防抖
query 仍作输入框受控绑定,debounced_query 才是请求参数:停顿
300ms 无新输入才提交,击键重启 effect 使旧延时任务经 peek 比对
后静默过期。页码重置移入防抖提交点——放在 oninput 里会让 page
变化用旧关键词抢发一次请求。
2026-07-27 11:29:28 +08:00
xfy
0086f0f3ea feat(admin): 素材卡片「复制」更名「复制路径」
按钮复制的是 /uploads/ 相对路径而非完整 URL,名称如实反映行为;
title 同步为「复制图片相对路径」。
2026-07-27 11:23:57 +08:00
xfy
d610fd5837 feat(admin): 素材页「孤儿」更名为「未引用」
与「引用中」对仗更自然。覆盖统计行、筛选 tab、清理按钮、
清理结果横幅、卡片徽章及 purge 服务端消息;代码标识符
(orphan_count / purge_orphan_assets 等)保持不变。
2026-07-27 11:16:59 +08:00
xfy
a890257009 feat(admin): Pagination 组件支持页码跳转
当前页码渲染为可编辑输入框(仅回调式翻页且总页数 > 1 时):
聚焦编辑、回车跳转(自动夹取到合法范围,非法输入还原)、失焦回显。
posts / assets / trash 三个 signal 翻页页已接入 on_jump;
路由式翻页(comments、前台)不传 on_jump,页码保持纯文本。
2026-07-27 11:13:26 +08:00
xfy
e33e89c258 fix(assets): 工具栏右侧按钮沉入 FilterTabs 的 mb-6 区域紧贴横幅
工具栏 items-end 对齐的是 flex 项的 margin box:FilterTabs 带 mb-6 使容器
底端低于下划线 24px,右侧按钮组无底 margin 被一并拉到底端,距横幅仅 4px。
按钮组同加 mb-6(移除补偿用的 pb-1)后,按钮底边与 tabs 下划线齐平,
下划线→横幅、按钮→横幅、横幅→网格均为 24px。
2026-07-27 10:01:56 +08:00
xfy
b4bea7db0d fix(assets): 操作横幅上下间距不对称(40px/8px → 24px/24px)
FilterTabs 自带 mb-6 在 flex 工具栏内撑出 24px,再叠横幅 mt-4 形成 40px
空洞,而横幅下方仅网格 mt-2 的 8px。横幅改 mt-4 为 mb-6:上方回归 tabs
标准间距 24px,下方与网格 mt-2 margin 塌陷同为 24px,与 comments/system
页节奏一致。
2026-07-27 09:57:15 +08:00
xfy
55e4712f28 fix(assets): 删除返回「素材不存在」时刷新网格自愈过期数据
行已被其他会话/重建删除时,本页网格仍显示幽灵卡片(success:false 不触发
reload)。refs 为空的业务拒绝即「行已不在 DB」,此时同样 bump reload,
让网格收敛到服务端真实状态;引用中拒绝(refs 非空)不误刷新。
2026-07-27 09:46:46 +08:00
xfy
b98059edcc fix(assets): 引用徽标加 z-10 修复被 blur-img 展示层遮盖
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
灯箱改造(051ca94)把缩略图换成 .blur-img 双层结构后,.blur-img-full
自带 z-index:1,而徽标 span 是 z-index:auto,绘制序被图片压过,
导致「孤儿 / 被 N 篇引用」徽标在 DOM 中存在但不可见。

浏览器实测:修复前 elementsFromPoint 命中 blur-img-full,
修复后命中徽标本身,琥珀色孤儿药丸恢复显示。
2026-07-24 18:31:45 +08:00
xfy
99d33b54d2 fix(lightbox): 图集切换不重算几何导致图片压扁
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
gotoIndex 换图时只替换 src,target/baseW/baseH/transform 全部沿用
第一张图的几何,宽高比不同的图被套用错误的 scale(显示宽高比 =
自身宽高比 × 首图宽高比)。同时 .lightbox-img 的 object-fit: contain
与 transformFor 依赖的「位图填满布局盒」前提冲突,素材网格的方形
缩略图容器下首图即被压扁。

修复:
- swap() 在新图加载后按 naturalWidth/Height 重算 fitCentered 目标,
  布局盒设为 target 尺寸、transform 归位 scale(1,1),baseW/H 同步
  供关闭飞回动画复用
- 先换 src 再判 complete,避免用旧图的 complete 误判缓存命中
- 移除 .lightbox-img 的 object-fit: contain,恢复位图填满布局盒

新增回归测试:切换后几何必须按新图宽高比重算(红绿验证通过)
2026-07-24 18:15:11 +08:00
xfy
051ca947e3 feat(assets): 素材网格接入灯箱预览
- 缩略图改为与前台正文图一致的 .blur-img 双层结构(?w=20 占位 +
  ?thumb=300x300 展示层 data-src),点击由全局注入的 lightbox.js 接管;
  图集模式(当前页内 ←/→ 切换),灯箱加载原图(data-src 去 query)。
- 数据异步到达/筛选/翻页/刷新后由 use_effect 调 __initLightbox 重新绑定
  (双保险契约同 PostContent:__lightboxSelectors + 直接调用兜底)。
- lightbox.ts 补 data-lb-bound 幂等守卫:Dioxus keyed diff 复用节点时
  重复初始化会叠加 click 监听(点一次开两次);附回归测试。
- invoke_optional_global 提取为 src/utils/js.rs 共享,post_content 复用。

浏览器实测:47 张绑定齐全,原图打开/图集切换/Esc 关闭正常,
筛选重建与重建索引重取后重绑幂等(单次点击仅 append 一次 overlay)。
2026-07-24 17:39:34 +08:00
xfy
fa40f4bd95 fix(assets): uuid 参数序列化失败导致上传 500
tokio-postgres 将 $1::uuid 的参数推断为 uuid 类型,String 无法
序列化(缺类型桥接),INSERT assets 报 error serializing parameter 0,
补偿删文件后返回 500。list/delete/purge/rebuild 的 $1::uuid 与
ANY($1::uuid[]) 全是同一颗未引爆的雷。

正路:tokio-postgres 启用 with-uuid-1,SQL 侧去掉全部 ::uuid cast,
Rust 侧全程 Uuid 类型,仅在 serde DTO 边界(WASM 共享模型)转 String;
外部传入的 id 在边界 parse_str,非法 id 走业务错误而非 500。
2026-07-24 17:04:35 +08:00
xfy
8f75a64bfd fix(assets): SUM(size_bytes) 显式转 bigint 修复列表 500
Postgres 的 SUM(bigint) 返回 NUMERIC,tokio-postgres 反序列化为
i64 时类型不匹配,row.get(3) 直接 panic(release 的 panic=abort
下会崩掉整个进程)。COALESCE(..., 0)::bigint 修正。
2026-07-24 16:50:30 +08:00
xfy
45d70e1dc6 refactor(assets): 清理 lint,对齐项目惯例
- api/assets/mod.rs 按 posts/comments 惯例加
  #![allow(clippy::unused_unit, deprecated, unused_imports)]
  (#[server] 宏固有噪音)
- 页面交互导入按 target_arch = wasm32 门控;组件级放行非 WASM 未用绑定
- 合并 check_upload_dimensions 进 upload_dimensions(单函数返回尺寸,
  消除死代码),测试同步更名
2026-07-24 16:44:47 +08:00
xfy
5612f16425 feat(assets): 封面「从素材库选择」联动
- AssetPickerModal:素材网格(最新 60 + 搜索)单击回填封面 URL,
  内嵌「上传新图」(复用 upload_image_file,成功即选中,
  失败留在 modal 内提示),上传期间置位 cover_uploading 拦截保存
- CoverUploader 空态加「素材库」入口(与「输入链接」并列)
2026-07-24 16:32:43 +08:00
xfy
0c47e87d30 feat(assets): 全量重建索引
- rebuild_assets_index:扫 uploads/(跳过点目录)→ upsert assets
  (xmax=0 判别新增;技术字段 IS DISTINCT FROM 才更新,保留 alt,
  幂等重跑 updated=0)→ 删文件已消失的行 → 全表扫 posts 重建 refs
- 磁盘扫描与 header 尺寸读取移入 spawn_blocking
- extract_asset_paths 提升 pub(crate) 供重建复用同一提取逻辑
- 前端「重建索引」按钮(进行中禁点,结果进横幅)
2026-07-24 16:27:02 +08:00
xfy
799455da43 feat(assets): 删除保护、孤儿清理与 alt 编辑
- delete_asset:引用中业务拒绝(Ok(success:false) + 引用文章列表),
  孤儿硬删除(文件 + DB 行级联 refs + 派生缓存)
- purge_orphan_assets:一键清理无引用且超过 7 天保护窗的素材,
  容忍单项文件删除失败,返回数量/释放字节/失败数
- update_asset_alt:管理性 alt 编辑
- invalidate_asset_caches:IMAGE_CACHE 前缀批量失效 + 尺寸缓存失效;
  磁盘派生缓存(哈希命名不可枚举)由既有清理任务回收
- 前端:卡片操作区三态(复制/alt/删除 ↔ alt 编辑 ↔ 确认删除),
  顶栏「清理孤儿」两步确认(带数量与总大小),操作结果横幅
2026-07-24 16:23:00 +08:00
xfy
99b96e7a0c feat(assets): 素材管理只读列表页
- list_assets:引用状态筛选 + filename/alt 搜索 + 时间/大小排序 +
  分页(60/页),一次响应带齐 tabs 计数与可清理孤儿统计
- /admin/assets 网格页:?thumb=300x300 缩略图卡片 + 引用徽标,
  单路由客户端分页(对齐管理文章模式),导航加「素材」入口
2026-07-24 16:15:19 +08:00
xfy
0c811efd4d feat(assets): 素材注册表数据层
- migration 015:assets 元数据注册表 + asset_refs 引用关联表
- upload_image 落盘后登记 assets(失败补偿删文件);尺寸校验复用
  upload_dimensions 一次解析同时返回 (w,h)
- sync_asset_refs 镜像 sync_tags:文章 create/update 事务内
  解析 content_html + cover_image 的 /uploads/ 路径重建引用
- Asset/AssetDto/AssetFilter/AssetSort 共享模型,id 以 String 承载
  (SQL 侧 ::uuid cast,避免 uuid crate 进 WASM 构建)
2026-07-24 16:07:07 +08:00
xfy
adde92e15c docs(assets): 素材管理功能设计规格
grilling 访谈确认的结论落盘:assets/asset_refs 双表、
sync_tags 模式引用同步、引用中禁删 + 孤儿 7 天窗一键清理、
手动重建索引回填、封面联动(编辑器正文选择器不做)。
2026-07-24 15:26:56 +08:00
xfy
de4bfafb72 chore: release v0.6.2
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
v0.6.2
2026-07-24 14:02:10 +08:00
xfy
2b048b94a3 fix(utils): sleep_ms 的 cfg guard 对齐 feature 模型,修复 web-only 构建失败
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
sleep_ms 原用 #[cfg(not(target_arch = "wasm32"))] guard tokio::time::sleep,
但 tokio 是 server-only 的 optional 依赖。此 guard 会在「非 wasm32 主机 + 仅
web feature」组合(如 cargo build --no-default-features --features web)下
误激活,此时 tokio 未引入,导致编译失败。

该 bug 长期被 [dev-dependencies] tokio 掩盖——cargo test / 开发构建总能拿到
tokio,只有发布/生产构建(排除 dev-deps)才暴露。符合 AGENTS.md 第 1 条
dual-target gating 标准:触及 server-only 依赖的代码必须用 #[cfg(feature = "server")]
而非 target_arch 区分。

修复:tokio 版本改为 #[cfg(all(feature = "server", not(target_arch = "wasm32")))],
并为无效构建组合「非 wasm32 且非 server」补一个 panic stub(该组合不是有效部署
目标,web feature 真实构建目标即 wasm32;stub 仅保证符号可编译,运行时不会触发)。

调用点 runner.rs 的 server 占位分支(通过 sleep_ms 继承同一问题)无需改动——
它只在 server target 下编译运行,sleep_ms 修复后其调用自然可编译。
2026-07-24 10:52:51 +08:00
xfy
5a10b07bff fix(router): 修复错误冒泡触发的 404 页「返回首页」卡死
PostDetail 对不存在的 slug 抛出 ServerFnError(404) 后,ErrorLayout 的
ErrorBoundary 捕获错误并在 fallback 渲染 NotFound。此时 ErrorBoundary 持有错误,
根据 Dioxus 0.7.9 的渲染逻辑(有错则永远渲染 fallback,不渲染 children/Outlet),
点击 Link 切换路由虽更新了 URL,但页面仍停留在 fallback,表现为「URL 变了页面不变」。

根因:ErrorBoundary 捕获错误后不会自动恢复,必须显式调用 clear_errors() 才能
重新渲染 children。原实现用 Link 导航无法干预点击时机清除错误。

修复:NotFound 与 ErrorLayout 的通用错误 fallback 中,「返回首页」改用 button,
onclick 内先 clear_errors() 清除错误边界,再 navigator().push 导航。
- NotFound 通过 try_consume_context::<ErrorContext>() 获取可能存在的错误边界:
  路由匹配命中的 404 无错误(clear_errors 是 no-op),错误冒泡的 404 才需清除。
- ErrorLayout 的 handle_error 闭包参数本身就是 ErrorContext,clone 后在按钮内使用。
2026-07-24 10:34:18 +08:00
xfy
f9cf1f71e4 chore: release v0.6.1
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
mhchem panic 修复 + 非测试代码 unwrap 消除的 patch 发布。

### Changed
- 消除非测试代码中的裸 unwrap(),改写为带不变量说明的 expect()

### Fixed
- mhchem 转译器三处 panic(正则转录、多字节字符越界、re! 宏降级)
- Docker 构建缺 patches/ 目录导致 pnpm install ENOENT
v0.6.1
2026-07-23 18:29:54 +08:00
xfy
d80664db5e docs(agents): 新增规范 #16——非测试代码禁止 unwrap
panic="abort" 让进程级 panic 无法恢复,裸 unwrap 与之冲突。新增第 16 条
明确:

- 默认走 ? / Result,经 AppError 或 Option 映射失败
- .expect("reason") 仅用于真正的不变量,消息必须解释为何不可能失败
- LazyLock 编译期常量、WASM 浏览器上下文、#[cfg(not(server))] stub、
  bin 工具与 #[cfg(test)] 为豁免集
- 预留 clippy unwrap_used/expect_used lint 的 allow 范围
2026-07-23 18:26:24 +08:00
xfy
690e00e194 refactor(unwrap): 消除非测试代码中的裸 .unwrap(),改为带不变量说明的 .expect()
panic="abort" 下任何 panic 直接杀进程,与 unwrap 的「稍后处理」语义冲突。
本轮把所有可证不变量的裸 unwrap 改写为带说明的 expect,并修复 time.rs 中
u32→i32 转换在大 ms 值下的潜在溢出 panic(clamp 到 i32::MAX)。

- src/utils/time.rs: sleep_ms 的 ms.try_into().unwrap() 改为 clamp 到
  i32::MAX,避免超大延时值溢出;window/set_timeout 的 expect 补充上下文说明
- src/utils/text.rs: 6 个静态正则的 unwrap 改为 expect,统一说明「编译期校验」
- src/api/{auth,comments/helpers}.rs: EMAIL_REGEX 同上
- src/api/markdown.rs: IMG_RE / TABLE_RE 同上
- src/api/rate_limit.rs: NonZeroU32::new(val.max(1)) 与 Quota::with_period
  补充不变量说明
- src/api/image.rs: HeaderValue::from_str(&etag).unwrap() 改为 expect 说明
  etag 仅含 ASCII hex;复用 etag_value 避免 clone
- src/middleware.rs: 静态 302 重定向响应的 unwrap 改为 expect
- src/components/post/post_content.rs、src/theme.rs: WASM 上下文 window().unwrap()
  改为 expect 说明仅浏览器执行

保留的 unwrap/expect(合理不变量,已在注释中说明):
- DB_POOL LazyLock 闭包(已有详细注释 + validate_database_url 前置校验)
- csrf.rs 静态 forbidden 响应
- highlight.rs syntect 内置 Plain Text 语法
- bin/generate_highlight_css 构建工具
- comments/* 的 #[cfg(not(server))] unreachable!() WASM stub
2026-07-23 18:26:19 +08:00
xfy
d1013dfa08 refactor(mhchem): re! 宏编译失败降级为不匹配,补上 panic=abort 缺失的防御
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
re! 宏原先 LazyLock::new(|| Regex::new($pat).unwrap()),在 panic="abort" 下
任何正则转录错误(如曾经的 ". __* " InvalidClass)都会直接杀进程,且 to_tex
的 catch_unwind 在 abort 模式下无法兜住。

引入 compile_or_log(pat) -> Option<Regex>:编译失败记 tracing::error 日志
并返回 None(绝不 panic)。封装为 CompiledPat(Option<Regex>) newtype,把
「坏正则降级为不匹配」的语义集中在一处:

  - captures_head(input) -> Option<Captures>:坏正则/未匹配统一 None
  - is_match(input) -> bool:坏正则/未匹配统一 false

re! 宏改为产出 &CompiledPat,调用方直接 re!("...").captures_head(...) /
.is_match(...),无需再到处写 re_ref(...)? 或手写 None 回退。状态机遇到
坏模式自然退到 "else" 兜底分支产出原样字符,不影响整体渲染。

适配点:Pat::Re、pat_match_head、match_pattern 的 re_match 闭包、
pat_enumber/state_of_aggregation/amount/formula、1/2 动作、mhchem_tables.rs
的 ce_action、3 个 static LETTERS/GREEK/ONE_GREEK_RE。

回归测试 compile_or_log_degrades_bad_regex_without_panic 断言坏正则返回 None
不 panic。clippy 干净,622 测试全过。
2026-07-23 18:16:22 +08:00