feat: use xpier media library for uploads and markdown embeds

Store media:ID in post bodies instead of CDN URLs, and keep sablog attach tokens as a fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
gouki
2026-09-16 19:03:52 +00:00
co-authored by Cursor
parent f6a9a8ea50
commit f81397a289
38 changed files with 5390 additions and 2059 deletions
+32 -29
View File
@@ -10,17 +10,17 @@
## 技术栈
| 组件 | 版本 | 说明 |
|------|------|------|
| Laravel | ^12.0 | 应用框架 |
| Filament | ^5.7 | 后台面板(zh_CN) |
| Livewire | ^4.4 | 前台交互 |
| Workerman | ^5.2 | 常驻进程:队列消费者 + WebSocket 进度推送 |
| yansongda/pay | ^3.7 | 支付宝 / 微信支付 |
| spatie 库 | — | permission / medialibrary / sitemap / robots / tags / backup |
| league/commonmark | ^2.9 | Markdown 渲染(GFM) |
| league/html-to-markdown | ^5.1 | 老 HTML 内容转换 |
| MySQL | 8.x | 数据库 |
| 组件 | 版本 | 说明 |
| ----------------------- | ----- | ------------------------------------------------------------ |
| Laravel | ^12.0 | 应用框架 |
| Filament | ^5.7 | 后台面板(zh_CN) |
| Livewire | ^4.4 | 前台交互 |
| Workerman | ^5.2 | 常驻进程:队列消费者 + WebSocket 进度推送 |
| yansongda/pay | ^3.7 | 支付宝 / 微信支付 |
| spatie 库 | — | permission / medialibrary / sitemap / robots / tags / backup |
| league/commonmark | ^2.9 | Markdown 渲染(GFM) |
| league/html-to-markdown | ^5.1 | 老 HTML 内容转换 |
| MySQL | 8.x | 数据库 |
## 快速开始
@@ -74,8 +74,8 @@ S3_ACCESS_KEY=... S3_BUCKET=... S3_ENDPOINT=... php artisan attachments:sync-s3
- 目录:`plugins/{vendor}.{name}/`,含 `plugin.json` + `src/ServiceProvider.php`(继承 `App\Blog\Support\PluginServiceProvider`)+ 可选 `database/migrations`、`routes/web.php`、`views/`
- 钩子:`addAction(hook, cb)` / `addFilter(hook, cb, priority)`,核心钩子:
- `comment.created`(AI 审核)、`payment.paid`(按 payable 分发订阅激活/解锁)、`post.rendered`(付费内容过滤/paywall)、`seo.structured_data`
- `filament.post_form` / `filament.post_table`(后台文章表单/表格注入)
- `comment.created`(AI 审核)、`payment.paid`(按 payable 分发订阅激活/解锁)、`post.rendered`(付费内容过滤/paywall)、`seo.structured_data`
- `filament.post_form` / `filament.post_table`(后台文章表单/表格注入)
- 插件依赖 `requires` 强制校验:启用需依赖已启用,停用/卸载会被依赖者阻止;启动时依赖不满足的插件跳过
- 订单商品化:`Payment` 多态 `payable` 关联购买实体,实体实现 `payableLabel()` / `payableUrl()` 接入订单展示(详见 docs/plugins.md)
- 后台「插件管理」:启停 / 上传 ZIP 安装 / 卸载 / 市场拉取(配置 `MARKET_URL` 后可用)
@@ -85,7 +85,7 @@ S3_ACCESS_KEY=... S3_BUCKET=... S3_ENDPOINT=... php artisan attachments:sync-s3
- 每篇文章有 `content_format`(markdown / html),新文章默认 Markdown
- 老数据导入标记为 html 原样保留;`content:convert` 批量转 Markdown
- 附件短代码:HTML 内容用 `[attach=1]` / `[img=1]`,Markdown 内容用 `{{attach:1}}` / `{{img:1}}`(`[]` 是 Markdown 保留字符),渲染时统一解析
- 附件短代码:新图 `![alt](media:ID)` / `[name](media:ID)`;HTML `[media=ID]`。老附件 HTML `[attach=1]` / `[img=1]`,Markdown `{{attach:1}}` / `{{img:1}}`。不要把 CDN 地址写进正文(见 [docs/media.md](docs/media.md))。
- 付费内容:`[paid]...[/paid]` 短代码,非会员/未解锁访客隐藏(会员插件)
## 异步处理(Workerman)
@@ -98,8 +98,8 @@ php artisan workerman:serve stop
- 消费者复用 Laravel `Queue\Worker`,**database / redis 队列统一支持**,常驻进程内框架只启动一次(省去每个任务的启动开销)
- 任务统一实现 `App\Blog\Jobs\AiJob` 接口(`handle(LlmClient)`),依赖由容器自动注入
- 队列选择:
- database(默认):`WORKERMAN_QUEUE_CONNECTION=database`
- redis:`WORKERMAN_QUEUE_CONNECTION=redis` 并确保 `QUEUE_CONNECTION=redis`、`REDIS_*` 配置正确;`redis` 连接的 `block_for` 建议设为 `0`(配合 1s 轮询,避免阻塞 Workerman event loop)
- database(默认):`WORKERMAN_QUEUE_CONNECTION=database`
- redis:`WORKERMAN_QUEUE_CONNECTION=redis` 并确保 `QUEUE_CONNECTION=redis`、`REDIS_*` 配置正确;`redis` 连接的 `block_for` 建议设为 `0`(配合 1s 轮询,避免阻塞 Workerman event loop)
- 消费队列:`WORKERMAN_QUEUES=default,ai`(ai 队列为 AI 审核/润色任务)
- 失败重试:`WORKERMAN_MAX_TRIES`(默认 3),超时 `WORKERMAN_TIMEOUT`(默认 60s),重试耗尽进 `failed_jobs` 表
- 降级路径:`php artisan queue:work` 照常可用(同一队列)
@@ -107,6 +107,7 @@ php artisan workerman:serve stop
## 外链中转(/go,内置插件)
内置插件 `neatstudio.link-tracker`(外链中转):
- 正文外链、评论正文 URL、评论作者网址、友链统一改写为 `/go/{hash}`:302 跳转 + **点击统计**、可**停用拦截**(返回 410)、隐藏真实链接、为未来广告位预留
- 站内链接/相对路径/mailto/tel/锚点不中转;同一 URL 复用同一短码
- 后台「管理 → 外链中转」可查看点击量、启停、删除
@@ -128,25 +129,27 @@ php artisan workerman:serve stop
## 测试
```bash
php artisan test # 84 个用例:迁移/老路由 301/主题/插件/会员支付流/AI 审核/商城购买
php artisan test # Feature + Unit: import, legacy URLs, themes, plugins, membership, media:ID render
```
## 开发者文档
| 文档 | 内容 |
|------|------|
| [docs/install.md](docs/install.md) | 安装指南(环境/配置/验收清单) |
| [docs/import.md](docs/import.md) | Sablog 老数据导入(命令/映射/附件/301 兼容) |
| [docs/deploy.md](docs/deploy.md) | 部署(nginx/systemd/workerman/cron/上线清单) |
| [docs/development.md](docs/development.md) | 开发规范(代码/插件/主题/队列/测试/提交) |
| [docs/tutorial-plugin.md](docs/tutorial-plugin.md) | 插件开发教程(10 分钟上手,配套 demo) |
| [docs/tutorial-theme.md](docs/tutorial-theme.md) | 主题开发教程(10 分钟上手,配套 demo) |
| [docs/plugins.md](docs/plugins.md) | 插件 API 详解(钩子/依赖/商品化/市场契约) |
| [docs/themes.md](docs/themes.md) | 主题 API 详解(视图解析/注入点/变量) |
| [docs/architecture.md](docs/architecture.md) | 架构与设计决策 |
| [docs/api.md](docs/api.md) | 对外 API(小程序/第三方) |
| 文档 | 内容 |
| -------------------------------------------------- | --------------------------------------------- |
| [docs/install.md](docs/install.md) | 安装指南(环境/配置/验收清单) |
| [docs/import.md](docs/import.md) | Sablog 老数据导入(命令/映射/附件/301 兼容) |
| [docs/deploy.md](docs/deploy.md) | 部署(nginx/systemd/workerman/cron/上线清单) |
| [docs/development.md](docs/development.md) | 开发规范(代码/插件/主题/队列/测试/提交) |
| [docs/tutorial-plugin.md](docs/tutorial-plugin.md) | 插件开发教程(10 分钟上手,配套 demo) |
| [docs/tutorial-theme.md](docs/tutorial-theme.md) | 主题开发教程(10 分钟上手,配套 demo) |
| [docs/plugins.md](docs/plugins.md) | 插件 API 详解(钩子/依赖/商品化/市场契约) |
| [docs/themes.md](docs/themes.md) | 主题 API 详解(视图解析/注入点/变量) |
| [docs/architecture.md](docs/architecture.md) | 架构与设计决策 |
| [docs/media.md](docs/media.md) | Media library + Markdown `media:ID` |
| [docs/api.md](docs/api.md) | 对外 API(小程序/第三方) |
**上手示例**:
- 插件:`plugins/demo.hello-world/`(短代码 / 钩子 / 路由 / 后台页 / 迁移 / 表单注入,全演示)
- 主题:`themes/demo/`(最小可运行主题,绿色系,覆盖 partials 即可换肤)