Grtsinry43 的前端札记
文章
技术学习

MemeDock:一个纯 Vibe Coding 的表情包管理器

2026年10月23日(更新于 2026年10月30日) 15 分钟阅读 浏览 0 喜欢 0 评论 0
AI 摘要

方寸表情,藏斗图之趣,亦生寻觅之烦;五端架构,蕴原生之美,更显工程之艰。

本文记录作者 18 天打造 MemeDock 的实践。面对表情包分散与格式兼容难题,他选择 Rust 核心共享 + 各端原生 UI,弃用 WebView / Flutter / KMP,借 UniFFI 桥接 Swift、Kotlin、C# 等。核心实现 SHA-256 去重、Telegram 贴纸转动态 WebP、按需转换导出。工程采用 Cargo workspace 分层,FFI 边界只传路径与元数据,并将 FFmpeg / libvpx / webp / rlottie 交叉编译为静态库。五端构建各有章法:Linux 直接引用 core,Android 自定义 Gradle 插件,macOS 用 SwiftPM,iOS 打包 XCFramework,Windows 以 vcpkg overlay 编译。最终借 AI 辅助完成多端原生交付,验证个人维护复杂项目的可行性。

说来也是奇怪,写这个项目的原因是和同事斗图的时候感觉不是很方便,尤其是表情还很分散不好找到。。所以有了这个很离谱的项目。

写下这篇文章的时候,我刚刚还在忙着想办法写 iOS 平台,然后把想要的功能迁过去,顺手看了一下提交历史,第一次提交是在 10 月 5 号,到今天(10 月 23 号)正好 18 天。

这十几天的业余时间里,我写了 Rust 的核心,通过 UniFFI 编译到各个平台,之后陆陆续续把 Android(Jetpack Compose)、Linux(GTK4 / Libadwaita)、macOS(SwiftUI / AppKit)、和正在写的 iOS(SwiftUI)和 Windows(WinUI 3) 五个端的原生界面大概完善好,同时用 Axum + SvelteKit 写了个自建的同步服务/WebUI

https://github.com/grtsinry43/MemeDock https://github.com/grtsinry43/MemeDock

说实话,如果是半年前,要我一个人在两三周里同时写 Rust、Kotlin、Swift、C#、GTK4 外加一套 C/C++ 混合交叉编译的媒体管线,那我肯定直接放弃了。但是时代变了嘛。毕竟现在有了 AI 之后,说实话,我会觉得,在整个我的工作流定型之后,写这样一个项目,感觉还是比较可控的

LINK

我是怎样使用 AI Agent 写代码的

从 AI 辅助编码、Vibe Coding,到现在的混合开发工作流。

简单来说,这篇文章会讲两个方面。 一是我在技术选型上纠结了半天,为什么最后选了「Rust 核心共享 + 各端纯原生」这么一条路,以及从 UniFFI 桥接到五个平台编译、打包、链接的完整实现细节;二是我自己怎么配合 AI 去推进这样一个多端项目。


为什么要做这个项目

理由很简单,我的表情包太乱了。最近摸鱼和同事发表情包的时候, 感觉找起来很麻烦,而且好多软件会有格式上的各种限制,比如说 WebM、WebP 这种的兼容性也不好,也有软件发送 PNG 之后会被强制转为黑底。

所以,当时我想的功能大概是这样的:

  1. 存储:所有导进来的图按 SHA-256 去重存在本地,原文件只读;
  2. 表情导入:用一个 Telegram 贴纸包链接就能直接抓取预览,自动把 .tgs 和 .webm 转成带透明通道的动态 WebP;支持剪贴板粘贴、文件拖入和 Android 系统分享;以后还可能,比如说扫本地的 QQ 的缓存文件,或者其他软件的文件来导入。
  3. 发图的时候按需转换:在复制、拖拽或者调起系统分享的时候,可以根据设置实时转出原图、兼容 PNG(1024px)、白底 PNG、小尺寸 JPEG(512px)或者兼容 GIF。

功能想清楚了,所以该选什么技术栈好呢?


技术选型:为什么选择各端原生呢?

在这个项目的技术栈上,我其实犹豫了非常久。

其实我个人还是在前端上占比比较多,但是各个语言都算有所涉猎吧。整个大概的语言分布,就按 GitHub 的来说是这样:

去年写 RSS 阅读器 PureFlow 的时候,我用的是 Kotlin Multiplatform (KMP) + Compose Multiplatform。当时觉得一套代码同时编译到 Android、iOS 和桌面端确实很爽。

LINK

PureFlow 简析:用 Kotlin 跨平台构建 RSS 阅读器

基于 Kotlin Multiplatform 和 Compose Multiplatform 技术栈,一套代码同时支持多平台的早期探索。

但是对于这个需求,每个分析下来是这样的。

首先是 Webview 方案(Electron / Tauri)。这个最先排除。好吧,我一点都不喜欢 WebView。作为一个稍微有点追求的开发者,我还是不希望用 WebView 来跨平台。这个东西理论上性能确实高,但是它占的内存,包括原生体验,我觉得都是比较难受的。

然后是 KMP (Compose Multiplatform) 和 Flutter。KMP 的问题是,它在桌面上整个跑在 JVM 平台上,本来就比较笨重;然后调原生的能力其实也就在桌面上,还是很难受的,和系统的配合也不是很好。主要最大的问题还是冷启动吧,就是在我想要使用这个软件的时候,要开一个很重的 JVM,这个无法接受。

而对于 Flutter 来说,其实我是比较讨厌它调那些原生方法,要做各种的 channel,然后写起来也十分的麻烦嘛。如果你在它的给的范围内用 Dart 来写,无论是逻辑还是 UI 还是很舒服的。但是,一旦涉及到这种原生操作,其实 Flutter 的体验还是没有那么好的。

而且,表情包管理工具在每个平台上的交互习惯完全不一样:

  • 在 macOS 和 Windows 上,核心操作是快捷键呼出、直接把图片文件从窗口里拖出去(Drag & Drop)、写入系统剪贴板;
  • 在 Android 和 iOS 上,核心操作是系统相册选择器、底部标签栏、下拉手势,以及调起系统的分享面板(Share Sheet);
  • 在 Linux(Wayland / GNOME)上,我希望能直接融入 Libadwaita 的原生视觉和传输协议。

。。好吧,这样看来纠结了一圈,最后还是决定干脆 UI 不做跨平台,而用 Rust 做一个共享的核心,然后各个端都使用自己的原生框架去写。只要我们逻辑共享就可以了

方案Electron / TauriFlutter / KMP (Compose)Rust Core + UniFFI + 各端原生 UI
运行开销与体积偏重,带着 Webview 的固有开销中等,自带渲染引擎和运行时最轻,无 GC 核心 + 系统自带 UI
调系统原生能力(拖拽/剪贴板/分享)受限于浏览器 API,需要折腾桥接调原生接口比较繁琐,桌面端支持一般最直接,本身就是系统原生代码
各端界面体验网页感重移动端还行,桌面端还是有割裂感原汁原味(SwiftUI / GTK4 / Compose / WinUI 3)
接入 C/C++ 媒体库走 Node 原生模块或拉子进程需要跨多层 JNI / C 互操作Rust 直接静态链接,进程内统一管理
以前手写的维护成本低中极高,一个人很难同时顾得过来
现在配合 AI 的成本低中可控,核心规范在 Rust,原生 UI 交给 AI 写很快

说真的,我不得不再次感叹 AI。如果放在以前,个人开发者选最后一条路的话,就是死路一条。就不说别的,在语言之间切换的上下文就足够痛苦了。但现在有 AI Agent,情况就不太一样了。只要我把数据模型、SQLite 存储、文件哈希去重、Telegram 转码、任务取消和错误处理在 Rust 实现好,那 AI 只管绘制 UI、展现状态,然后接入原生 API 了。

而 SwiftUI、Jetpack Compose、WinUI 3 这些声明式 UI,恰好是现在 Claude 和 Codex 最擅长写的部分呀


从打地基开始

那就先从项目结构开始吧。整个仓库是一个 Cargo workspace,大概的结构是这样的

text
MemeDock/
├── crates/
│   ├── domain/          # 强类型 ID、模型、导出规则与契约(不依赖数据库、异步运行时和图像库)
│   ├── storage/         # SQLite (SeaORM)、原子文件暂存 (StagedFile) 与 SHA-256 内容寻址存储
│   ├── core/            # 业务用例、任务调度、媒体转码管线、Telegram 导入、备份归档与同步逻辑
│   ├── ffi/             # 给 Swift / Kotlin / C# 用的 UniFFI 绑定、DTO 转换与事件流
│   └── api-contracts/   # 同步服务端与客户端共享的数据结构
├── tools/
│   ├── native-media/    # FFmpeg / libvpx / libwebp / rlottie 交叉编译脚本
│   ├── uniffi-bindgen/  # 锁定版本的 Kotlin / Swift 绑定生成器
│   ├── macos/           # macOS 打包、图标生成与 Mach-O 对齐修复脚本
│   ├── ios/             # iOS 静态库合并与 XCFramework 构建脚本
│   └── windows/         # Windows vcpkg overlay 与 C# 桥接构建脚本
└── apps/
    ├── android/         # Kotlin + Jetpack Compose + 自研 Gradle 构建插件
    ├── linux/           # Rust + GTK4 + Libadwaita(不走 FFI,直接调 memedock-core)
    ├── macos/           # Swift 6 + SwiftUI & AppKit
    ├── windows/         # C# + WinUI 3 (Preview) + uniffi-bindgen-cs
    ├── ios/             # Swift 6 + SwiftUI (进行中)
    └── server/          # Axum + 内嵌 SvelteKit 前端(自建同步服务)

在讲各个平台怎么编译之前,先看两个前提条件吧。

1. 编译器规则

现在有了 AI,写 Rust 会简单一些;但是 AI 很多情况下都喜欢为了图省事到处塞 .unwrap(),或者在遇到生命周期问题时乱上 unsafe。 这里如果错误处理搞不好,那客户端就闪退了呀。

所以这里简单做了下规范吧

toml
[workspace.lints.rust]
unsafe_code = "forbid"

[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "deny"

2. Telegram 贴纸转码

MemeDock 支持直接使用 Telegram 的贴纸包链接或者名字,然后按需选择导入

Telegram 的贴纸有三种:普通的静态 .webp、用 Gzip 压缩的 Lottie 矢量动画 .tgs,还有 VP9 编码的 .webm 视频。 于是,这里要维护一个逻辑在 crates/core/src/images/telegram.rs 里把 .tgs 和 .webm 统一转成了带透明通道的动态 WebP。

这里转码的能力来源于 FFmpeg。针对它进行了裁剪,打包进项目。这样就站在前人的肩膀上,完成了方便的转换。


跨平台桥接与编译

聊完 Rust 核心,接下来就是整个项目最折腾的部分了,我们需要把 Rust 产物编译到各个平台能直接用的样子。

第一步:设计 crates/ffi 与 UniFFI 绑定机制

首先是 crates/ffi/Cargo.toml。因为五个平台对链接方式的要求完全不同,我给 memedock-ffi 同时开了三种 crate-type:

toml
# crates/ffi/Cargo.toml
[package]
name = "memedock-ffi"
version.workspace = true

[lib]
crate-type = ["rlib", "cdylib", "staticlib"]

[dependencies]
memedock-core = { path = "../core" }
memedock-domain = { path = "../domain" }
tokio = { workspace = true, features = ["macros"] }
uniffi.workspace = true

这三种类型分别对应不同的端:

  • rlib:给纯 Rust 写的 Linux 客户端直接源码级引用;
  • cdylib:给 Android(.so)、macOS(.dylib)和 Windows(.dll)做动态库加载;
  • staticlib:给 iOS(.a)做静态库归档,最后打进 .xcframework。

在写 FFI 导出接口的时候,可以直接用 UniFFI 的过程宏。在 crates/ffi/src/lib.rs 结尾加上一行 uniffi::setup_scaffolding!(),然后把要暴露给原生端的对象和 DTO 标上 #[derive(uniffi::Object)]、#[derive(uniffi::Record)] 和 #[uniffi::export]:

rust
// crates/ffi/src/library.rs
#[derive(uniffi::Object)]
pub struct LibraryHandle {
    pub(crate) inner: memedock_core::Library,
}

#[uniffi::export]
pub async fn open_library(configuration: LibraryConfiguration) -> Result<Arc<LibraryHandle>> {
    Ok(Arc::new(LibraryHandle {
        inner: memedock_core::Library::open(configuration.try_into()?).await?,
    }))
}

#[uniffi::export]
impl LibraryHandle {
    pub fn request_thumbnail(&self, id: String, priority: Priority) -> Result<Arc<ThumbnailTask>> {
        Ok(ThumbnailTask::new(
            self.inner.request_thumbnail(id.parse()?, priority.into())?,
        ))
    }

    pub fn export_original(&self, id: String) -> Result<Arc<ExportTask>> {
        Ok(ExportTask::new(self.inner.export_original(id.parse()?)?))
    }
    // ...
}

我们这里要刻意地保持 FFI 边界只传文件路径和轻量元数据 DTO,绝不传图像像素 Buffer。 比如上面这个 request_thumbnail,它返回的 ThumbnailTask 最终给到 Kotlin / Swift / C# 的只是一个本地生成的缩略图文件路径,而不是几兆的 RGBA 字节数组。这样跨语言边界几乎没有内存拷贝开销,上层拿到路径后直接喂给系统的 Coil(Android)或 ImageIO(iOS/macOS)去解码上屏。

因为用了过程宏,编译出来的 .so / .dylib / .a / .dll 二进制文件内部就已经自带了完整的接口元信息(Metadata)。 可以封装一个非常简单的入口来干这件事情

rust
// tools/uniffi-bindgen/src/main.rs
fn main() {
    uniffi::uniffi_bindgen_main();
}

第二步:把 FFmpeg / libvpx / libwebp / rlottie 交叉编译成静态库

在编译 memedock-ffi 之前,前置需要的是四个 C/C++ 媒体库编译出来。

在类 Unix 平台(Linux、Android、macOS、iOS)上,可以写一个 tools/native-media/build.sh 。它会先根据传入的 Rust target 识别平台架构和编译器工具链:

bash
# tools/native-media/build.sh
case "$target" in
  x86_64-unknown-linux-gnu) arch=x86_64; vpx_target=x86_64-linux-gcc; abi= ;;
  aarch64-linux-android) arch=aarch64; vpx_target=arm64-linux-gcc; abi=arm64-v8a ;;
  x86_64-linux-android) arch=x86_64; vpx_target=x86_64-linux-gcc; abi=x86_64 ;;
  aarch64-apple-darwin) arch=arm64; vpx_target=arm64-darwin23-gcc; abi=; darwin=1 ;;
  x86_64-apple-darwin) arch=x86_64; vpx_target=x86_64-darwin23-gcc; abi=; darwin=1 ;;
  aarch64-apple-ios) arch=arm64; vpx_target=arm64-darwin-gcc; abi=; darwin=1; ios=1; sdk=iphoneos ;;
  aarch64-apple-ios-sim) arch=arm64; vpx_target=arm64-iphonesimulator-gcc; abi=; darwin=1; ios=1; sdk=iphonesimulator ;;
esac

同时,为了不让安装包体积膨胀,FFmpeg 的 configure 参数被我砍到了只剩解码 WebM 贴纸必需的几个模块:

bash
"$base/sources/ffmpeg/configure" ${ffmpeg_args[@]+"${ffmpeg_args[@]}"} --prefix="$prefix" \
  --disable-everything --disable-autodetect --disable-programs --disable-doc --disable-debug \
  --disable-avdevice --disable-swresample --disable-network --disable-shared \
  --enable-static --enable-pic --enable-libvpx --enable-decoder=libvpx_vp8,libvpx_vp9 \
  --enable-avfilter --enable-filter=fps \
  --enable-demuxer=matroska --enable-parser=vp8,vp9 --enable-protocol=file --disable-x86asm

首先 --disable-everything,只开 matroska 解复用、libvpx_vp8/vp9 解码,再加一个 fps 滤镜(用来修复部分 Telegram WebM 贴纸里时间戳重复导致的播放鬼畜问题)。


第三步:各个平台的编译和接入

媒体库编好之后,然后就是想办法放到各个平台了。

1. Linux 端(GTK4 + Libadwaita)

Linux 端(apps/linux)本身就是用 gtk4-rs 和 libadwaita-rs 写的 Rust 程序,所以它根本不需要走 memedock-ffi,这个最简单了,直接在 Cargo.toml 里把 memedock-core 引进来就行:

toml
# apps/linux/Cargo.toml
[package]
name = "memedock-linux"

[[bin]]
name = "memedock"
path = "src/main.rs"

[dependencies]
memedock-core = { path = "../../crates/core" }
memedock-domain = { path = "../../crates/domain" }
gtk4.workspace = true
libadwaita.workspace = true

编译的时候一行命令就完事了:

bash
cargo build --release --locked -p memedock-linux

Cargo 会 管理前面的构建流程,把 FFmpeg、libvpx、rlottie 的 .a 静态库和 memedock-core 一起链接进单个原生可执行文件 target/release/memedock 里。配合 packaging/arch/ 下的 PKGBUILD 打成包,在 GNOME / Wayland 下跑起来跟系统自带应用没有任何区别。


2. Android 端(Jetpack Compose):自定义 Gradle 插件

思路依然是把前面构建的部分交给构建系统管理,那安卓就交给 Gradle 管理,所以这里在 apps/android/build-logic 写了一套 Gradle 插件(MemeDockRustPlugin)

kotlin
// apps/android/build-logic/src/main/kotlin/RustBuildTasks.kt
val args = mutableListOf(cargoNdk.get().asFile.absolutePath, "ndk")
for (abi in abis.get()) args.addAll(listOf("--target", abi))
args.addAll(listOf(
    "--platform", "28",
    "--output-dir", outputDirectory.get().asFile.absolutePath,
    "build", "--locked", "-p", "memedock-ffi"
))
if (release.get()) args.add("--release")

exec.exec {
    workingDir(repository)
    environment("ANDROID_NDK_HOME", ndk.absolutePath)
    // Android 15+ 要求原生库按 16 KB 页面大小对齐
    environment("CARGO_TARGET_AARCH64_LINUX_ANDROID_RUSTFLAGS", "-C link-arg=-Wl,-z,max-page-size=16384")
    environment("CARGO_TARGET_X86_64_LINUX_ANDROID_RUSTFLAGS", "-C link-arg=-Wl,-z,max-page-size=16384")
    environment("CARGO", "cargo")
    commandLine(args)
}.assertNormalExitValue()

.so 编好之后,GenerateBindingsTask 会调用宿主机编出来的 memedock-bindgen 生成 Kotlin 代码,然后自动挂进 Android Library 的源码目录里:

kotlin
// apps/android/build-logic/src/main/kotlin/MemeDockRustPlugin.kt
variant.sources.kotlin?.addGeneratedSourceDirectory(generated) { it.outputDirectory }
variant.sources.jniLibs?.addGeneratedSourceDirectory(native) { it.outputDirectory }

这样上层的 Jetpack Compose 模块只需要依赖 :rustBridge,就能直接像调普通 Kotlin 挂起函数一样调 Rust 核心了。

很爽吧,这就是丝滑的混合开发


3. macOS 端(SwiftUI & AppKit)

macOS 端(apps/macos)用 Swift Package Manager 管理

swift
// apps/macos/Package.swift
targets: [
    .systemLibrary(name: "MemeDockBridgeFFI", path: "Generated/FFI"),
    .target(
        name: "MemeDockBridge",
        dependencies: ["MemeDockBridgeFFI"],
        path: "Generated/Bridge"
    ),
    .executableTarget(
        name: "MemeDock",
        dependencies: ["MemeDockBridge"],
        path: "Sources/MemeDock",
        linkerSettings: [
            .linkedLibrary("memedock_ffi")
        ]
    ),
]

这里我们写了一个打包脚本, tools/macos/build.sh 先用 Cargo 编译出 libmemedock_ffi.dylib,再用 memedock-bindgen 从这个 .dylib 生成 MemeDockBridge.swift、MemeDockBridgeFFI.h 和 module.modulemap 放进 Generated/ 目录,然后调 swift build 编译出可执行文件。


4. iOS 端(SwiftUI):用 libtool 缝合静态库与双架构 XCFramework

到了 iOS 端(apps/ios),这里不打算折腾动态库签名,而是直接走 staticlib(静态库 .a)打包进 XCFramework。

在 tools/ios/build-bridge.sh 里,脚本会针对真机(aarch64-apple-ios)和模拟器(aarch64-apple-ios-sim)分别调 cargo rustc 编译静态库:

bash
# tools/ios/build-bridge.sh
SDKROOT="$sdkroot" IPHONEOS_DEPLOYMENT_TARGET=18.0 \
  "CC_$target=$(xcrun --sdk "$sdk" --find clang)" \
  "CXX_$target=$(xcrun --sdk "$sdk" --find clang++)" \
  "CFLAGS_$target=-target $clang_target -isysroot $sdkroot" \
  "CXXFLAGS_$target=-target $clang_target -isysroot $sdkroot" \
  BINDGEN_EXTRA_CLANG_ARGS="-target $clang_target -isysroot $sdkroot" \
  cargo rustc --locked -p memedock-ffi --lib --crate-type staticlib --target "$target"

这样 Xcode 里只需要引用 MemeDockNative.xcframework 和生成的 MemeDockBridge.swift,不管切模拟器还是切真机,一行额外配置都不需要改。


5. Windows 端(C# + WinUI 3):vcpkg Overlay 补丁与 uniffi-bindgen-cs

最后是 Windows 端(apps/windows)。在 Windows 上用 MSVC 工具链编译 FFmpeg 和 rlottie,如果去拉 MSYS2 跑 bash 脚本会非常慢且容易出环境问题,所以我用了 vcpkg + Overlay Ports 的方式。

在 tools/windows/prepare-media.ps1 里,脚本会拉取固定 commit 的 vcpkg,然后动态修改 ffmpeg 的 portfile.cmake,把前面那套 --disable-everything 的最小化解码参数塞进去:

powershell
# tools/windows/prepare-media.ps1
$originalOptions = 'set(OPTIONS "--enable-pic --disable-doc --enable-runtime-cpudetect --disable-autodetect")'
$minimalOptions = 'set(OPTIONS "--disable-everything --disable-network --enable-pic --disable-doc --enable-runtime-cpudetect --disable-autodetect")'
$ffmpegPort = $ffmpegPort.Replace($originalOptions, $minimalOptions)

$decoderOptions = 'string(APPEND OPTIONS " --enable-decoder=libvpx_vp8,libvpx_vp9 --enable-demuxer=matroska --enable-parser=vp8,vp9 --enable-protocol=file --enable-filter=fps --disable-x86asm")'
$ffmpegPort = $ffmpegPort.Replace($optionsLog, $decoderOptions + "`n" + $optionsLog)

而在编译 Rust 的时候,crates.io 上的 rlottie 0.5.4 源码里居然无条件写了一行 use std::os::unix::ffi::OsStrExt;,在 Windows 下直接编译失败。所以我把 rlottie 源码放进了仓库的 third-party/rlottie,修掉那行 Unix 引用后,在根 Cargo.toml 里用 [patch.crates-io] 做了替换。

等 cargo build -p memedock-ffi --target x86_64-pc-windows-msvc 编出 memedock_ffi.dll 之后,再用 uniffi_bindgen_cs 生成 memedock_ffi.cs。而在 C# 的桥接工程 MemeDock.Bridge.csproj 里,直接把生成的 .cs 文件和运行时的 .dll 挂进去:

xml
<!-- apps/windows/bridge/MemeDock.Bridge.csproj -->
<ItemGroup>
  <Compile Include="$(NativeRoot)\generated\memedock_ffi.cs" Link="Generated\memedock_ffi.cs" />
  <None Include="$(NativeRoot)\runtime\*.dll" Link="%(Filename)%(Extension)"
        CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

这样上层的 WinUI 3 应用只要引用 MemeDock.Bridge.csproj,编译输出目录里就会自动带上 memedock_ffi.dll 和裁剪后的 FFmpeg .dll,C# 代码直接通过 P/Invoke 调用底层的 Rust 核心。


五端编译与交付产物汇总

。。。讲到这里,已经打得手指酸了 不过我们确实实现了一套核心代码处处运行

平台媒体库编译方式Rust 产物类型绑定生成方式最终交付给 UI 工程的桥接产物
Linuxbuild.sh 编为静态 .arlib (源码直连)无需 FFI,直接调 memedock-core单个原生二进制 memedock-linux
Androidbuild.sh + NDK 编为 .a 并静态合入cdylib (libmemedock_ffi.so)Gradle 插件调 memedock-bindgenjniLibs/<abi>/*.so + 生成的 Kotlin (JNA) 源码
macOSbuild.sh 编为 .a 并静态合入cdylib (libmemedock_ffi.dylib)memedock-bindgen修好 Mach-O 对齐与 @rpath 的 .dylib + Swift/C 头文件
iOSbuild.sh + xcrun 编为 .astaticlib (libmemedock_ffi.a)libtool 合并 .a + memedock-bindgen双架构 MemeDockNative.xcframework + Swift 源码
Windowsvcpkg + Overlay Ports (MSVC)cdylib (memedock_ffi.dll)memedock-windows-bindgen (uniffi_bindgen_cs)memedock_ffi.dll + 媒体 .dll + MemeDock.Bridge.dll


Vibe Coding 过程

emm 就是大概的开发节奏吧

GIT LOG

MemeDock 18 天开发记录

2026-10-05

搭建地基与 Android 首版

完成 domain / storage / core / ffi 四层结构,接通 UniFFI 和 Android 基础图库

2026-10-06

跑通完整功能闭环

做好合集标签、回收站、导出预设、单文件备份恢复与多语言

2026-10-07

Linux 客户端与批量操作

写完 GTK4 + Libadwaita 客户端,加上多选与批量归类

2026-10-09

Telegram 贴纸包导入

搞定 FFmpeg + rlottie 静态编译,实现 .tgs / .webm 转动态 WebP

2026-10-12

macOS 客户端与打包

适配 Apple Darwin 媒体栈,完成 SwiftUI 桌面端、快捷键与 DMG 打包工作流

2026-10-16

Windows WinUI 3 客户端

搞定 MSVC 下媒体库构建,接通 C# 桥接、Fluent 界面与剪贴板/拖出

2026-10-18

自建同步服务端

用 Axum + SvelteKit 写好同步服务与管理后台,四端接通增量同步

2026-10-23

推进 iOS 客户端

打包双架构 XCFramework,完成本地导入、搜索、详情预览与系统标签栏框架

写在最后

以前一个人写项目,总得在“套壳凑合用”和“没精力写多端原生”之间妥协。现在有了 AI 帮忙分担各端 UI 和构建脚本的体力活,确实一个人维护的成本低了很多

目前 MemeDock 的 macOS、Linux、Android 都已经发了 Release,iOS 端还在继续收尾系统分享和整理功能 😋。如果你平时也有攒表情包的习惯,或者对这套跨平台方案感兴趣,欢迎去 GitHub 试试看,觉得好用的话也可以点个 Star~

https://github.com/grtsinry43/MemeDock https://github.com/grtsinry43/MemeDock

喜欢 0
评论区在赶来的路上...