1. 产品概述
产品名称:Inserable
目标平台:macOS
技术栈:Swift + SwiftUI
核心功能:管理 Hugo 博客的图片资源,自动重命名并生成 Markdown 格式的图片引用文本
解决的问题:Hugo 博客中图片管理混乱、命名不规范、手动编写图片引用繁琐
2. 核心概念
2.1 层级结构
Inserable
├── 根目录 1 (Root Folder 1)
│ ├── Session A (文件夹 A)
│ │ ├── formatA_001.png
│ │ ├── formatA_002.png
│ │ └── ...
│ ├── Session B (文件夹 B)
│ │ └── ...
│ └── ...
└── 根目录 2 (Root Folder 2)
├── Session X (文件夹 X)
│ └── ...
└── ...
2.2 核心关系
| 概念 | 说明 |
|---|---|
| 根目录 (Root Folder) | 用户的 Hugo 图片存储路径,如 /Users/alex/myblog/static/images/。最多支持 2 个。 |
| Session | 与根目录下的一个文件夹形成 1:1 绑定关系。Session 名 = 文件夹名。 |
| 图片 | Session 文件夹内的图片文件,按统一格式命名。 |
2.3 命名规则
图片命名格式:<formating_1>_<###>.<ext>
<formating_1>:用户为每个 Session 指定的命名前缀<###>:三位数补零序号(001, 002, 003…),按图片创建时间排序,Session 内独立计数<ext>:统一转换为 PNG 格式
Markdown 输出格式:

3. 数据完整性机制
3.1 镜像备份
| 项目 | 说明 |
|---|---|
| 触发时机 | 每 5 分钟自动执行一次 |
| 存储位置 | Application Support/Inserable/Mirror/ |
| 保留版本 | 仅保留最新一份 |
| 镜像内容 | 根目录结构(Session 文件夹名 + 文件夹内图片文件名列表) |
3.2 完整性验证
验证内容:
- 根目录级别:Session 文件夹名称 + 数量
- Session 级别:图片文件名称 + 数量
验证时机:
- 程序启动时
- 每次镜像时(每 5 分钟)
- 用户手动点击 “IN SYNC” 按钮时
不一致处理 (Integrity Error): 检测到任何外部修改触发 ERROR,锁定受影响区域。用户需在弹窗选择以下操作之一:
| 选项 | 行为 |
|---|---|
| Restore from Mirror (恢复镜像) | 将磁盘文件恢复到镜像记录的状态 (撤销外部修改)。 |
| Delete Session (删除 Session) | 移除该 Session,文件夹移动到回收站。 |
| Re-sync (Accept Changes) (重新同步) | 以当前磁盘状态为准,更新元数据和镜像。 |
Re-sync (Accept Changes) 详细逻辑:
- 扫描:扫描当前 Session 文件夹。
- 分类处理:
- 合规文件 (符合
<formating_1>_<###>.png):保留,更新元数据。 - 非合规图片:强制标准化。视为"新添加图片",分配新序号(当前最大序号+1),转 PNG,重命名,
originalPath标记为 “unknown (re-synced)"。 - 非图片文件:弹窗提示用户手动移除,或选择自动移动到回收站。
- 更新状态:更新
nextSequenceNumber并立即写入磁盘。 - 快照:生成新镜像快照并解除锁定。
3.3 允许的例外
- .DS_Store 文件:自动忽略
- 同名文件内容替换:只验证文件名,不验证文件内容(允许用户在 Preview 中编辑图片后保存)
4. 根目录管理
4.1 根目录要求
- 最多支持 2 个根目录
- 根目录内只允许存在:文件夹 或 空
- 如检测到非文件夹文件(如散落的 .png),提示用户清理后才能继续
4.2 根目录初始化流程
用户选择文件夹
↓
扫描文件夹内容
↓
[存在非法文件?] → 是 → 提示用户清理 → 阻止继续
↓ 否
为每个子文件夹创建对应 Session(名称 = 文件夹名)
↓
按字母顺序排列 Sessions
↓
根目录初始化完成(Sessions 处于"待标准化"状态)
4.3 根目录更换
- 更换根目录 = 将所有 Session 文件夹迁移到新位置
- 旧根目录的所有数据(Sessions、镜像)清空后重建
5. Session 管理
5.1 Session 创建方式
方式 A:点击 “New Session” 按钮
- 在当前根目录下创建空文件夹,默认名 newsession<#>
- Session 列表中出现新条目
- 用户可重命名 Session(同步重命名文件夹)
- Session 处于空状态,等待用户拖入图片
方式 B:拖入文件夹到 “New Session” 按钮
- 暂存路径:程序读取并暂存该文件夹内所有图片的原始绝对路径。
- 用户输入:弹窗要求输入
<formating_1>。
- 用户取消 → 操作中止,清除暂存数据。
- 复制文件:复制文件夹到当前根目录。
- 原子性检查 (Atomic Safety Check):
- 验证目标文件是否存在且大小 > 0。
- 验证失败 → 回滚(删除已复制的不完整文件),中止流程,保留原文件夹。
- 标准化:执行标准化流程(使用暂存的原始路径写入元数据)。
- 源文件处理:仅在步骤 4 和 5 全部成功后,根据设置决定是否将原文件夹移动到回收站。
5.2 Session 标准化流程
当用户首次向空 Session 拖入图片,或从文件夹创建 Session 时:
弹窗要求输入 <formating_1>
↓
[用户取消?] → 是 → 操作中止,Session 保持原状
↓ 否
按创建时间排序所有图片
↓
转换所有图片为 PNG 格式
↓
重命名:<formating_1>_001.png, <formating_1>_002.png, …
↓
生成变更日志(首次标准化时)
↓
写入磁盘:立即写入 nextSequenceNumber 和元数据
↓
创建镜像快照
5.3 Session 删除
- 用户删除 Session → 文件夹重命名为
<原名>_Inserable_conflict后移动到回收站 - Session 数据从 Inserable 中移除
6. 图片操作
6.1 添加图片
操作方式:拖入图片到已选中的 Session 区域 处理流程:
- 暂存路径:程序读取并暂存图片的原始绝对路径。
- 前置检查:如果 Session 未标准化 → 先触发标准化流程。
- 复制文件:复制图片到 Session 文件夹。
- 原子性检查 (Atomic Safety Check):
- 验证目标文件是否存在且大小 > 0。
- 验证失败 → 回滚,中止流程,保留原图。
- 标准化处理:
- 转换为 PNG 格式。
- 分配下一个可用序号。
- 重命名为
<formating_1>_<###>.png。 - 将暂存的原始路径写入元数据。
- 立即持久化:立即将更新后的
nextSequenceNumber写入磁盘,不等待程序关闭。 - 源文件处理:仅在上述所有步骤成功后,根据设置决定是否将原图移动到回收站。
- 镜像:更新镜像。
6.2 查看图片
- 点击图片行(COPY 按钮以外区域)→ 调用系统 Preview.app 打开图片
6.3 图片格式转换
优先级:PNG > JPG > WEBP 规则:所有图片统一转换为 PNG 格式
7. 输出功能
7.1 单条复制
点击图片行的 COPY 按钮 → 复制该图片的 Markdown 文本到剪贴板。
7.2 全部复制 (Copy All)
首次标准化后的输出(包含变更日志): Markdown 引用 + Original Path 日志。 后续复制的输出(简洁版): 仅 Markdown 引用列表。
7.3 变更日志备份
存储位置:Application Support/Inserable/past_change_log/
文件命名:<session_folder>_<dd-mm-yyyy>_change_log_backup.txt
内容:首次标准化时的完整输出(包含 Original Path)
8. 全局设置
| 设置项 | 类型 | 说明 |
|---|---|---|
| 根目录 1 路径 | 路径选择 | 第一个 Hugo 图片根目录 |
| 根目录 2 路径 | 路径选择 | 第二个 Hugo 图片根目录 |
| 删除原图 | Checkbox | 拖入图片后是否将原图移动到回收站 |
| 删除原文件夹 | Checkbox | 拖入文件夹后是否将原文件夹移动到回收站 |
| 深色/浅色模式 | Toggle | UI 主题切换 |
9. UI 布局
9.1 主界面结构
┌─────────────────────────────────────────────────────────────┐
│ ● ● ● │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────────────────────────────┐ │
│ │ ROOT_FOLDER │ │ SESSION NAME │ │
│ │ (可点击切换) │ │ ┌─────────────────────────────────┐│ │
│ ├─────────────┤ │ │ COPY │ pic_001.png │││ │
│ │ │ │ ├─────────────────────────────────┤│ │
│ │ SESSION 1 │ │ │ COPY │ pic_002.png │││ │
│ │ SESSION 2 │ │ ├─────────────────────────────────┤│ │
│ │ SESSION 3 │ │ │ COPY │ pic_003.png │││ │
│ │ ... │ │ │ ... ││ │
│ │ │ │ │ [滚动条]│ │
│ │ │ │ └─────────────────────────────────┘│ │
│ │ │ └─────────────────────────────────────┘ │
│ │ ┌─────────┐ │ │
│ │ │NEW SESS.│ │ │
│ │ └─────────┘ │ │
│ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ [IN SYNC] [SETTINGS] [COPY ALL] │
└─────────────────────────────────────────────────────────────┘



9.2 状态指示
| 状态 | 显示 | 说明 |
|---|---|---|
| 正常 | IN SYNC(绿色) | 数据完整性正常 |
| 同步中 | 旋转图标 | 正在执行镜像操作 |
| 错误 | ERROR(红色) | 检测到完整性问题 |
9.3 Integrity Error 界面
- 主界面模糊化,显示 “INTEGRITY ERROR”。
- 弹窗选项:
- Restore from Mirror: 恢复到上次同步状态。
- Re-sync (Accept Changes): 接受当前磁盘状态(强制标准化非合规文件)。
- Delete Session: 删除此 Session。
10. 首次启动流程
启动 Inserable → 检测配置 → (未配置)显示空界面 → 用户点击 Root Folder → 选择文件夹 → 初始化根目录 → 完成。
11. 错误处理
11.1 根目录错误
| 错误类型 | 触发条件 | 处理方式 |
|---|---|---|
| 非法文件 | 根目录下存在非文件夹文件 | 阻止选择,提示用户清理 |
| Session 名称冲突 | 创建新 Session 时名称已存在 | 弹窗询问是否替换(旧文件夹移动到回收站) |
| 完整性错误 | Session 文件夹名称或数量变化 | 锁定整个根目录,要求修复 |
11.2 Session 错误
| 错误类型 | 触发条件 | 处理方式 |
|---|---|---|
| 非法文件 | Session 文件夹内存在非图片文件 | 提示用户清理或移动到回收站 |
| 完整性错误 | 图片文件名或数量变化 | 锁定该 Session,要求修复 |
12. 数据存储
12.1 存储位置
~/Library/Application Support/Inserable/
├── config.json (全局设置)
├── sessions/ (Session 元数据)
├── mirror/ (镜像数据)
└── past_change_log/ (变更日志备份)
12.2 Session 元数据结构
{
"sessionName": "Banff_Travel",
"formattingPrefix": "banff_picture",
"nextSequenceNumber": 5,
"lastUpdated": "2026-01-14T15:30:00Z",
"isStandardized": true,
"files": [
{
"currentName": "banff_picture_001.png",
"originalName": "IMG_1190.jpeg",
"originalPath": "/Users/alex/Downloads/IMG_1190.jpeg",
"sequenceNumber": 1,
"addedAt": "2026-01-14T15:25:00Z"
}
]
}
13. MVP 范围外(未来扩展)
- 支持超过 2 个根目录
- Session 昵称
- 自定义图片格式/Alt text
- 批量重命名已有图片
- 多版本镜像历史
推荐项目文件结构 (Project Structure)
为了确保开发过程清晰且符合上述设计逻辑,建议采用以下 Swift 项目结构:
Inserable/
├── App/
│ ├── InserableApp.swift # 入口
│ └── AppDelegate.swift # 生命周期处理
├── Model/
│ ├── AppConfig.swift # 全局设置模型
│ ├── Session.swift # Session 元数据模型 (Codable)
│ ├── ImageFile.swift # 图片文件模型 (Codable)
│ └── IntegrityStatus.swift # 枚举:Synced, Syncing, Error
├── ViewModel/
│ ├── RootViewModel.swift # 根目录管理,Session 列表逻辑
│ ├── SessionViewModel.swift # 具体 Session 的图片操作逻辑
│ └── SettingsViewModel.swift # 设置页面逻辑
├── View/
│ ├── MainLayout/
│ │ ├── SidebarView.swift # 左侧 Session 列表
│ │ └── ImageListView.swift # 右侧图片列表
│ ├── Components/
│ │ ├── ImageRowView.swift # 单行图片组件
│ │ └── StatusBadge.swift # In Sync/Error 指示器
│ ├── Overlays/
│ │ ├── IntegrityErrorView.swift # 错误处理弹窗 (含 Re-sync)
│ │ └── NewSessionPopup.swift # 创建/标准化弹窗
│ └── SettingsView.swift
├── Service/
│ ├── FileSystemManager.swift # 核心:处理复制、删除、原子性检查
│ ├── ImageProcessor.swift # 处理格式转换 (PNG)、重命名
│ ├── PersistenceManager.swift # 处理 JSON 读写 (立即写入逻辑)
│ ├── MirrorManager.swift # 处理 5分钟定时镜像
│ └── IntegrityChecker.swift # 处理比对逻辑、Re-sync 扫描
├── Utils/
│ ├── Extensions.swift # String, URL, Date 扩展
│ ├── Logger.swift # 简单的调试日志
│ └── PathHelper.swift # 处理 App Support 路径
└── Resources/
├── Assets.xcassets
└── Info.plist
LOG
项目开发开始
Jan14, 1:06am
项目雏形完成
Jan 14, 1:28am
存在bug:
镜像还原过程存在问题,没有还原到目标位置。这整个功能完全是废掉的。镜像还原后,会出现幽灵文件夹。具体看bug4。
初始化文档只有最初对第一个session进行标准化完成后的第一次复制,才会复制成功且输出格式为:

Original Path:
(/Users/kircerta/Desktop/Inversable_TESTING/MYBLOG_static/images/Apple_Vision_Pro_Article/Body_1.png)
============================================================

Original Path:
(/Users/kircerta/Desktop/Inversable_TESTING/MYBLOG_static/images/Apple_Vision_Pro_Article/Body_2.png)
============================================================

Original Path:
(/Users/kircerta/Desktop/Inversable_TESTING/MYBLOG_static/images/Apple_Vision_Pro_Article/Body_3.png)
其余时间失败,在其它sesssion初始化时,输出均为:




- New Session 创建问题严重,具体表现为:
- 创建后会莫名其妙出现重命名。重命名应该只有右键时才会出现。
- 拖入文件夹后,session的创建要么是有延迟,要么逻辑有问题
- 其它问题待发掘
存在幽灵文件夹,不存在,但确实在session中。这个状态点击列表里的文件名不会出现预览,因为文件根本不存在。但也没有出现报错。手动sync也没有出现integrity报错,这点非常奇怪。
改动root文件及/删除root文件夹路径时,完全没有出现警告提醒。这是会直接把整个文件夹里session记录全部改掉的操作,完全需要用户二次确认。

- delete original image after import 选项toggle后完全无效,原因未知。
- folder toggle选项暂未测试,但推测也有问题
- Root Folder 切换按钮是坏的,点击会跳出选择root folder的选项。如果只有一个root folder没有第二个,按钮应该不允许点击。
- 并且,root folder现在1和2可以选择完全相同的folder,这是不应该出现的现象。
- root folder 2 根本就没读取进去。

- 我可以直接打开为Standardized的folder。关闭窗口后右侧内容并未被高斯模糊屏蔽并显示类似"finish set-up in pop-up window, reactivate pop up window here(此按钮后续加入)。

- session文件夹重命名貌似有延迟,但重命名后引导正确。
待改进内容
UI风格还是要参考Texmorph去做。目前看上去不太像SwiftUI的风格,反而像是Flutter.
sync 按钮动画有问题,会导致整个画面glitch,不符合期望,但不影响使用。
- 问题调查完发现是因为sync的速度太快,
- 弹出窗口内容显示不全,虽然目前不影响信息传递,但依然需要改进。

session设置完成后,初始设置无法被修改。期望里应该是可以被修改,然后一键应用。应用后留下一个修改log(格式参考开发文档1.0 alpha)的。现在没有这个功能。
standardize按钮显示有问题
如果session少了东西,整个session就必须被delete,这个目前来看逻辑没问题。但也许有更好的方案也说不定?
Jan14 开发总结
UI布局没问题,细节带打磨。在布局不改变的基础上,应用Texmorph的设计语言也许是个不错的思路。
依然有很多隐藏bug没测试,上述只是问题的一小部分。不过,程序能够运行这一点已经足够令人欣慰。
[to be continued…]
Jan16, 1:56am 于 Robart Commons
Inserable Alpha 0.0.2 开发日志与路线图总结:One-Tap Mapping 模块
日期:2026-01-16 模块:One-Tap Mapping Utility 状态:功能竣工 (Feature Complete) / 核心 Bug 已修复
1. 今日工作日志 (Dev Log)
今日主要完成了 One-Tap Mapping 功能的从无到有,并解决了 macOS 沙盒机制 (Sandbox) 与文件系统交互的一系列棘手问题。
修复与改进清单
A. 核心逻辑构建
- 功能实现:实现了从“散乱 Root Folder”到“Session Folder”的自动化整理逻辑。
- 文章级去重 (Deduplication):对于同一篇文章多次引用同一张图的情况,实现了智能识别,不再创建
img_1.png,img_2.png副本,而是复用文件。 - 智能格式保留:确立了“白名单”机制。
jpg/gif保持原样(防止动图变静帧或体积膨胀),仅将heic/webp等格式标准化为 PNG。
B. 权限与沙盒攻坚 (The Sandbox Battles)
这是今日最关键的 Debug 环节。
- 问题:App 只有读取权限,无法删除源文件,导致整理后出现“旧文件残留 + 新文件冗余”的双重占用。
- 修复 1 (显式授权):在
UtilityView中移除了文件夹路径自动预填。强制用户手动点击NSOpenPanel选择文件夹。这是获取 macOS 安全作用域写入权限 (Security-Scoped Write Permission) 的唯一正解。 - 修复 2 (权限锁):在
Manager代码中引入startAccessingSecurityScopedResource(),在操作期间显式持有文件锁,防止系统拦截写操作。 - 修复 3 (输出重定向):将日志、备份和未使用的图片统一移动到
Downloads文件夹,规避了 Desktop 文件夹的高级权限限制。
C. 算法健壮性提升
- URL 解码修复:修复了 Markdown 中
image%202.png无法匹配本地image 2.png的 Bug。现在算法会先进行 URL Decode 再匹配文件。 - Gatekeeper 兼容:将物理删除 (
removeItem) 改为移入废纸篓 (trashItem)。这不仅符合 macOS 安全规范,也为用户提供了最后的“后悔药”。
2. 未来改进建议 (Future Improvements)
虽然功能已跑通,但为了追求极致体验,以下方向值得在后续迭代中考虑:
Technical Debt & Optimization
- 性能优化:目前采用单线程遍历。若 Root Folder 图片数量破万,可能会阻塞 UI。建议未来引入 Swift
TaskGroup进行并行文件处理。 - 事务回滚 (Atomic Operations):目前依靠
.bak文件备份。未来可引入文件操作的事务机制,一旦中间出错,自动还原所有移动过的文件(类似数据库 Rollback)。
UX/UI Enhancements
- 结果导向:操作完成后,可以在 UI 上增加一个 “Show Log Folder” 按钮,直接打开 Finder 定位到下载目录的日志位置。
- 忽略列表:允许用户配置
.inignore文件,指定某些散落在根目录的文件不被 One-Tap Mapping 处理(例如固定的 Logo 或背景图)。
3. 用户说明书执行方案 (Documentation Plan)
根据讨论,我们放弃在 App 内硬编码说明书,转为**“静态网站 + 客户端入口”**的轻量化方案。
执行步骤
内容生产 (Content)
- 来源:直接复用你现有的
content_zh/posts/Developer/Inverable开发文档.md。 - 完善:将今天 One-Tap Mapping 的操作逻辑(特别是“必须手动选择文件夹以授权”这一点)补充进去。
- 来源:直接复用你现有的
部署 (Deployment)
- 使用 Hugo 将其编译为静态网站。
- 部署到你的 GitHub Pages 或个人服务器。
客户端集成 (App Integration)
- 入口:在 App 顶部菜单栏
Help->Inserable Documentation。 - 代码实现:
CommandGroup(replacing: .help) { Button("文档") { NSWorkspace.shared.open(URL(string: "你的网址")!) } }
- 入口:在 App 顶部菜单栏
兜底提示 (Fallback)
- 在
UtilityView的文件夹选择器下方,保留一行小字提示:“注意:请务必手动点击选择按钮,以授予 App 清理旧文件的权限。”
- 在
4. 结语
Inserable 的 One-Tap Mapping 模块现已具备了**“零残留、零误判、有迹可循”**的工程标准。今天的 Debug 有力地证明了在 macOS 开发中,顺应 Sandbox 规则比对抗它更有效。
Mission Accomplished.
以上Gemini总结。