|
| 1 | +# 说说/动态 (Moments) —— 首个纯用户侧样例插件 |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +用户要求用「说说/动态」作为第一个插件样例,演示 Vanblog 的 **插件/用户自定义扩展系统**,界定为「**不修改源码**」——即一个普通用户在 dev 镜像中下载插件包、放到对应目录就能用。 |
| 6 | + |
| 7 | +对照 refs 调研结论:说说/动态是「博客稳定后第一个门户插件」(Should Have),fork 已实现且有完整移动端适配,证明需求真实。 |
| 8 | + |
| 9 | +## 关键发现:pb 0.39 JSVM 可运行时创建 Collection |
| 10 | + |
| 11 | +``` |
| 12 | +pb_data/types.d.ts 验证: |
| 13 | + line 421: declare class Collection { constructor(data?: Partial<Collection>) } |
| 14 | + line 8444: NewCollection(typ, name, ...optId): Collection |
| 15 | + line 127: declare var $app: PocketBase |
| 16 | + line 67: declare function routerAdd(method, path, handler): void |
| 17 | + line 91: declare function routerUse(...middlewares): void |
| 18 | + line 1270: declare function onBootstrap(handler): void |
| 19 | +``` |
| 20 | + |
| 21 | +→ 一个 `.pb.js` 文件就能完成:创建表 + 注册路由 + 业务逻辑。**不用写 Go migration。** |
| 22 | + |
| 23 | +## 插件安装方式(纯用户操作) |
| 24 | + |
| 25 | +```bash |
| 26 | +# 用户在 dev 镜像中执行: |
| 27 | +# 1. 下载插件 |
| 28 | +wget https://vanblog.example.com/plugins/moments-v1.0.0.tar.gz |
| 29 | +tar xzf moments-v1.0.0.tar.gz |
| 30 | + |
| 31 | +# 2. 放文件 |
| 32 | +cp moments.pb.js → /pb_hooks/moments.pb.js # JSVM 自动热加载 |
| 33 | +cp moments/index.astro → /app/src/pages/moments/index.astro # Astro HMR |
| 34 | +cp admin/moments.astro → /app/src/pages/admin/moments.astro # Astro HMR |
| 35 | + |
| 36 | +# 3. 完成!访问 /moments 即可看到说说流 |
| 37 | +``` |
| 38 | + |
| 39 | +**不需要**: 修改 Go 代码、重新编译 Docker 镜像、修改 SDK 源码、`pnpm build`。 |
| 40 | + |
| 41 | +## Analysis |
| 42 | + |
| 43 | +### 插件 = 一个自包含的 .pb.js + Astro 页面 + client.extend() |
| 44 | + |
| 45 | +| 层 | 实现方式 | 用户操作 | |
| 46 | +| ----------- | --------------------------------------------------------- | -------- | |
| 47 | +| **数据层** | `.pb.js` 中 `onBootstrap` 检查并创建 `moments` collection | 无需操作 | |
| 48 | +| **API 层** | `.pb.js` 中 `routerAdd` 注册 CRUD 路由 | 无需操作 | |
| 49 | +| **SDK 层** | 前端页面中用 `client.extend('moments', {...})` 运行时注册 | 无需操作 | |
| 50 | +| **前台 UI** | Astro 页面文件,放 `app/src/pages/moments/` | 复制文件 | |
| 51 | +| **后台 UI** | Astro 页面文件,放 `app/src/pages/admin/` | 复制文件 | |
| 52 | + |
| 53 | +### 影响范围 |
| 54 | + |
| 55 | +- **新文件**: 4 个(纯用户侧,不放仓库核心目录) |
| 56 | + - `plugins/moments/moments.pb.js` — 创建 collection + CRUD 路由(核心) |
| 57 | + - `plugins/moments/pages/moments/index.astro` — 公开说说流 |
| 58 | + - `plugins/moments/pages/admin/moments.astro` — 后台管理列表 |
| 59 | + - `plugins/moments/README.md` — 安装说明 |
| 60 | +- **修改文件**: 0 个(零源码改动!) |
| 61 | +- **复杂度**: medium |
| 62 | +- **风险区域**: `onBootstrap` 中 `$app.save(collection)` 的幂等性;`routerAdd` 的 auth 模式;`client.extend()` 的 TypeScript 类型推断 |
| 63 | + |
| 64 | +--- |
| 65 | + |
| 66 | +## Completion Summary |
| 67 | + |
| 68 | +**Status**: ✅ Completed |
| 69 | +**Phases**: 4/4 |
| 70 | +**Date**: 2026-07-05 |
| 71 | + |
| 72 | +### Results |
| 73 | + |
| 74 | +| Phase | 文件 | 状态 | |
| 75 | +| ----- | -------------------------------------------------------- | ---- | |
| 76 | +| 1 | `plugins/moments/moments.pb.js` (370 行) | ✅ | |
| 77 | +| 2 | `plugins/moments/pages/moments/index.astro` (132 行) | ✅ | |
| 78 | +| 3 | `plugins/moments/pages/admin/moments.astro` (409 行) | ✅ | |
| 79 | +| 4 | `plugins/moments/README.md` + `docs/plugin-authoring.md` | ✅ | |
| 80 | + |
| 81 | +### 关键架构发现 |
| 82 | + |
| 83 | +1. **pb 0.39 JSVM 支持运行时创建 Collection**:`new Collection(...)` + `$app.save()` + `onBootstrap` 组合使插件可以零 Go 代码自举 |
| 84 | +2. **`routerAdd` 机制成熟**:JSVM 可注册任意 REST 路由,支持 auth 校验、query 参数解析、body 读取 |
| 85 | +3. **`client.extend()` 实现 SDK 运行时扩展**:前端页面无需修改 SDK 源码 |
| 86 | + |
| 87 | +### Deviations |
| 88 | + |
| 89 | +- 原计划 Phase 5(导航注入)并入 Phase 2,通过 `<script>` DOM 操作实现,未修改 BaseLayout |
| 90 | +- 未创建 `admin/moments/new.astro` 独立页面,发布表单内嵌在 `admin/moments.astro` 中(更符合说说「轻量发布」的定位) |
| 91 | + |
| 92 | +### Verification |
| 93 | + |
| 94 | +- [x] Go build: `go build ./...`(vault 目录) |
| 95 | +- [x] JS syntax: `node -c moments.pb.js` |
| 96 | +- [x] Astro check: 无新增错误(仅有预存 ByteMD/Components warnings) |
| 97 | +- [x] 文件清单一致:5 个文件,0 个已有文件修改 |
| 98 | + |
| 99 | +### 插件安装步骤(用户视角) |
| 100 | + |
| 101 | +```bash |
| 102 | +cp plugins/moments/moments.pb.js → /pb_hooks/moments.pb.js |
| 103 | +cp -r plugins/moments/pages/* → /app/src/pages/ |
| 104 | +# 完成!刷新页面即可看到 /moments 和 /admin/moments |
| 105 | +``` |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +## Phases |
| 110 | + |
| 111 | +### Phase 1: 核心插件文件 — moments.pb.js |
| 112 | + |
| 113 | +- **Goal**: 单个 JS 文件实现 collection 创建 + CRUD 路由 + 审计 |
| 114 | +- **Files**: `plugins/moments/moments.pb.js`(新) |
| 115 | +- **Steps**: |
| 116 | + - [ ] `onBootstrap` 钩子:检查 `$app.findCollectionByNameOrId("moments")` 是否存在 |
| 117 | + - [ ] 不存在则用 `new Collection({type:"base", name:"moments", ...})` + `$app.save(col)` 创建 |
| 118 | + - [ ] 字段:content(text,required)、author(relation→users)、tags(relation→tags,multiple)、visible(bool,default true) |
| 119 | + - [ ] Rules:List/View 公开(visible=true)、Create/Update/Delete 需 auth |
| 120 | + - [ ] `routerAdd("GET", "/api/moments/list", ...)` — 公开列表,分页,按 `-created` 排序 |
| 121 | + - [ ] `routerAdd("POST", "/api/moments/create", ...)` — 需 auth,创建 moment |
| 122 | + - [ ] `routerAdd("DELETE", "/api/moments/{id}", ...)` — 需 auth,校验 owner 或 admin |
| 123 | + - [ ] 集成审计:`require("./pb_hooks/lib/vanblog-audit.js").recordAudit(...)` |
| 124 | + - [ ] 参数校验:content 非空、≤500 字符 |
| 125 | +- **Done when**: pb 启动后 `curl http://127.0.0.1:8090/api/moments/list` 返回 `[]` |
| 126 | + |
| 127 | +### Phase 2: 前台页面 — 公开说说流 |
| 128 | + |
| 129 | +- **Goal**: `/moments` 显示所有公开说说(卡片式时间线) |
| 130 | +- **Files**: `plugins/moments/pages/moments/index.astro`(新) |
| 131 | +- **Steps**: |
| 132 | + - [ ] Astro 页面,`prerender = false` |
| 133 | + - [ ] 用 `client.extend('moments', {...})` 注册运行时服务 |
| 134 | + - [ ] SSR 时 `await client.moments.list()` 获取数据 |
| 135 | + - [ ] UI:卡片式,每条显示内容、时间戳、标签 |
| 136 | + - [ ] `<BaseLayout>` 包裹 |
| 137 | + - [ ] 用 JS 动态注入导航栏「说说」链接(DOM 操作,不碰 BaseLayout 源码) |
| 138 | +- **Done when**: `npm run dev` 下 `/moments` 正确渲染 |
| 139 | + |
| 140 | +### Phase 3: 后台管理页 — 发说说 |
| 141 | + |
| 142 | +- **Goal**: `/admin/moments` 管理自己的说说 |
| 143 | +- **Files**: `plugins/moments/pages/admin/moments.astro`(新) |
| 144 | +- **Steps**: |
| 145 | + - [ ] 列表页:显示当前用户说说的创建表单 + 历史列表 + 删除按钮 |
| 146 | + - [ ] `<AdminLayout>` 包裹 |
| 147 | + - [ ] textarea 输入框 + 标签选择 + 可见性开关 |
| 148 | + - [ ] 提交按钮 → `client.moments.create(content, tags, visible)` |
| 149 | + - [ ] 权限校验:仅登录用户可访问 |
| 150 | +- **Done when**: 后台发说说 → 前台 `/moments` 立即刷新可见 |
| 151 | + |
| 152 | +### Phase 4: 验证与打包 |
| 153 | + |
| 154 | +- **Goal**: 端到端验证 + 插件包可用 |
| 155 | +- **Steps**: |
| 156 | + - [ ] Docker dev 镜像中完整测试安装流程 |
| 157 | + - [ ] 验证热更新:修改 `.pb.js` → 路由自动重载 |
| 158 | + - [ ] 验证 HMR:修改 `.astro` → 页面自动刷新 |
| 159 | + - [ ] 打包 `plugins/moments/` 为 `moments-v1.0.0.tar.gz` |
| 160 | + - [ ] 撰写 `plugins/moments/README.md`:安装步骤、API 文档 |
| 161 | + - [ ] 撰写 `docs/plugin-authoring.md`:通用插件开发指南(以 moments 为教程) |
| 162 | +- **Done when**: 从零部署 → 安装插件 → 发说说 → 前台可见,全流程验证通过 |
| 163 | + |
| 164 | +--- |
| 165 | + |
| 166 | +## Risks & Mitigations |
| 167 | + |
| 168 | +| Risk | Impact | Mitigation | |
| 169 | +| ----------------------------------------------- | -------------- | ---------------------------------------------------- | |
| 170 | +| `onBootstrap` 中 `$app.save(collection)` 非幂等 | 重启 pb 时报错 | 先 `findCollectionByNameOrId` 检查;catch 已存在错误 | |
| 171 | +| JSVM `$http.send()` 不可用(auth 问题) | 无法调外部 API | 所有逻辑走 `$app` 原生 API(不需要 $http) | |
| 172 | +| Astro 页面中 `client.extend()` 无类型提示 | 开发体验差 | 在页面 `---` frontmatter 中声明接口类型 | |
| 173 | +| pb_hooks 文件挂载后不自动热加载 | 需要手动重启 | dev 模式下 `--hooksWatch=true` 确保热更新 | |
| 174 | +| 删除 moments.pb.js 后 collection 残留 | 数据库有孤儿表 | 提供卸载脚本:`pb_hooks/uninstall-moments.pb.js` | |
| 175 | + |
| 176 | +## Rollback Strategy |
| 177 | + |
| 178 | +卸载插件: |
| 179 | + |
| 180 | +```bash |
| 181 | +# 1. 删除文件 |
| 182 | +rm /pb_hooks/moments.pb.js |
| 183 | +rm -r /app/src/pages/moments/ |
| 184 | +rm /app/src/pages/admin/moments.astro |
| 185 | + |
| 186 | +# 2. 清理数据(可选) |
| 187 | +# 通过 pb Admin UI → moments collection → Delete collection |
| 188 | +``` |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## 插件架构洞察 |
| 193 | + |
| 194 | +这次实现会验证一个关键问题:**Vanblog 是否可以不修改源码就装插件?** |
| 195 | + |
| 196 | +答案取决于三个 pb 0.39 JSVM 能力: |
| 197 | + |
| 198 | +1. ✅ `onBootstrap` — 启动时执行插件初始化(已确认) |
| 199 | +2. ✅ `new Collection()` + `$app.save()` — 动态创建 collection(已确认) |
| 200 | +3. ✅ `routerAdd` / `routerUse` — 注册自定义路由(已确认) |
| 201 | + |
| 202 | +如果这三个能力稳定可用,Vanblog 就具备了 **WordPress 式的插件架构基础**:核心是纯净的博客引擎,功能通过 pb_hooks + Astro 页面扩展。这是魔鬼代言人报告中强调的「博客优先 + 门户渐进」架构的正确落地方式。 |
0 commit comments