内容
# 斯奎拉
> 高性能、AI 原生的 CMS,基于 Go 构建。内核 + 扩展架构。MCP 优先。
## 这是什么
斯奎拉是一个内容管理系统,围绕一个理念设计:**AI 应该能够在无需人类干预的情况下构建和管理整个网站。** 内核提供内容节点、渲染、身份验证和强大的 CoreAPI。其他所有功能(媒体、电子邮件、SEO、表单)都是扩展程序 - gRPC 插件,具有自己的数据、逻辑和管理 UI。
关键的区别在于:每个 CMS 操作都作为 MCP 工具公开。AI 代理可以通过结构化 API 创建节点类型、播种内容、激活主题和管理扩展程序 - 无需文件系统访问、无需 shell 命令、无需 HTML 抓取。
### 架构一句话
**内核 = Linux 内核**(仅基础设施)。**扩展程序 = Debian 软件包**(拥有自己的完整堆栈)。**管理 SPA = 浏览器 shell**(仅加载扩展微前端)。**主题 = 模板 + 脚本 + 资产**(在激活时注册,无需重启)。
## 技术栈
| 层 | 技术 |
|-------|-----------|
| 语言 | Go 1.24+ |
| HTTP | Fiber(路由,中间件) |
| ORM | GORM(PostgreSQL 16+) |
| 数据库 | PostgreSQL(JSONB,GIN 索引) |
| 管理 UI | React + TypeScript(Vite,Tailwind,shadcn/ui) |
| 模板 | Go `html/template` |
| 脚本 | Tengo(沙盒 VM,`core/*` 模块) |
| 插件 | HashiCorp go-plugin(gRPC,双向) |
| 存储 | 本地磁盘(计划使用 S3) |
| 安全 | 基于能力的权限,Ed25519 许可证验证 |
## 快速开始
```bash
# 克隆并使用 Docker 运行
git clone <仓库 URL> && cd squilla
docker compose up --build
# 应用程序在 http://localhost:3000 运行
# 管理界面在 http://localhost:3000/admin
# 默认登录:admin@example.com / changeme
```
### 环境变量
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `DB_HOST` | `localhost` | PostgreSQL 主机 |
| `DB_PORT` | `5432` | PostgreSQL 端口 |
| `DB_USER` | `squilla` | 数据库用户 |
| `DB_PASSWORD` | `squilla` | 数据库密码 |
| `DB_NAME` | `squilla` | 数据库名称 |
| `THEME_PATH` | `themes/default` | 活动主题路径 |
| `APP_ENV` | `production` | `development` 禁用模板缓存 |
| `PORT` | `3000` | HTTP 端口 |
| `DATABASE_URL` | _(未设置)_ | 可选:`postgres://user:pass@host:port/db?sslmode=disable`。设置后覆盖单个 `DB_*` 变量。 |
| `ADMIN_EMAIL` | `admin@squilla.local` | 自动播种管理员用户的电子邮件(仅首次启动)。 |
| `ADMIN_PASSWORD` | _(未设置)_ | 如果未设置,则在首次启动时生成一个随机密码并打印到应用程序日志中 **一次**。设置此值以跳过随机密码。 |
| `SQUILLA_SECRET_KEY` | _(未设置)_ | **静止加密的 AES-256 主密钥**。必须是 base64 字符串,解码后 **恰好 32 个原始字节**。使用 `openssl rand -base64 32` 生成。如果未设置,则秘密设置以明文存储(仅开发环境);生产环境启动拒绝启动。 |
| `SESSION_SECRET` | _(未设置)_ | 会话 cookie 签名密钥。任何足够随机的字符串。使用 `openssl rand -base64 48` 生成。生产环境中必填。 |
| `MONITOR_BEARER_TOKEN` | _(未设置)_ | `/api/v1/stats` 监控端点的 Bearer 令牌。任何不透明的字符串。生产环境中必填。 |
| `CORS_ORIGINS` | _(未设置)_ | 以逗号分隔的允许 CORS 的来源列表。默认为 Coolify 部署中的公共域。 |
## 在 Coolify 上部署
斯奎拉提供了一个 `coolify-compose.yml`,用于近乎零配置的部署:
1. 在 Coolify 中,创建一个新的 **资源 → 公共仓库**,并指向此仓库。
2. 构建包:**Docker Compose**。Compose 文件:**`coolify-compose.yml`**。
3. 为 `app` 服务设置 **域名**(Coolify 填充 `SERVICE_FQDN_APP` 并提供 TLS)。
4. 在环境变量选项卡中 **覆盖 `SQUILLA_SECRET_KEY`** - 请参见下面的注意事项。
5. 点击 **部署**。
Coolify 通过其 `SERVICE_*` 神奇变量自动生成数据库凭证、会话秘密和监视令牌 - 这些变量开箱即用。第一个管理员密码在首次启动时生成,并打印到 `app` 容器日志中 **一次**(在日志中搜索 `first-boot admin credentials`)。要预设凭证,请在首次部署之前将 `ADMIN_EMAIL` 和 `ADMIN_PASSWORD` 环境变量添加到 `app` 服务中。
### 注意事项:`SQUILLA_SECRET_KEY` 需要手动覆盖
Coolify 的 `SERVICE_BASE64_<NAME>` 神奇变量生成一个 **32 个字符的 base64 字符串**,解码后为 **24 个原始字节**。斯奎拉的秘密服务需要 **恰好 32 个原始字节**(AES-256 规范),因此自动生成的数值被拒绝并导致容器崩溃循环,报错为:
```
secrets init failed: SQUILLA_SECRET_KEY must be 32 raw bytes (base64-encoded): got 24 bytes, want 32
```
**修复:** 在首次部署之前,在 Coolify 环境变量选项卡中设置:
```bash
SQUILLA_SECRET_KEY=<粘贴 openssl rand -base64 32 的输出>
```
这会生成一个 44 个字符的 base64 字符串(32 个原始字节解码后),而秘密服务接受该字符串。验证:
```bash
echo -n "$SQUILLA_SECRET_KEY" | base64 -d | wc -c # 必须打印:32
```
`SESSION_SECRET` 和 `MONITOR_BEARER_TOKEN` 环境变量 **不受长度限制** - Coolify 的 `SERVICE_BASE64_64_*`(48 个原始字节)的值可以直接使用。
预构建的镜像发布到 `ghcr.io/erikkubica/squilla:latest`(多架构,amd64 + arm64)。
## 架构
### 内核 + 扩展
**硬规则:** 如果禁用/删除扩展程序会使核心代码遗留死代码,则该代码属于扩展程序,而不属于核心。
核心提供:
- 内容节点(CRUD,渲染,布局解析)
- 身份验证,会话,RBAC
- CoreAPI(35+ 方法,涵盖 15 个领域)
- 扩展系统(加载器,代理,迁移)
- 主题引擎 + 公共站点渲染
- 事件总线 + 过滤器链
- MCP 工具服务器
扩展程序拥有:
- Go 插件二进制文件(业务逻辑,HTTP 处理)
- Tengo 脚本(事件钩子,过滤器,路由)
- React 微前端(管理 UI)
- SQL 迁移(自己的表)
- 清单(声明功能,路由,菜单)
### CoreAPI
单个 Go 接口提供所有 CMS 功能。三个适配器:
1. **Tengo** (`core/*` 模块) - 用于 `.tgo` 脚本
2. **gRPC** (SquillaHost 服务通过 GRPCBroker)- 用于编译插件
3. **内部** (直接 Go 调用)- 用于核心代码
参见 `docs/extension_api.md` 以获取完整参考。
## MCP 工具(AI 接口)
斯奎拉在 17 个领域中公开了大约 75 个 MCP 工具,均位于 `core.<domain>.<verb>` 命名空间下。这是 AI 代理与 CMS 交互的方式。下表突出了每个领域中最常用的工具 - 调用 `core.guide` 以获取实时决策树、配方和 CMS 状态快照,或调用 `core.extension.standards` / `core.theme.standards` 以获取创作规则。
### 内容管理
| 工具 | 描述 |
|------|-------------|
| `core.node.create` | 创建内容节点 |
| `core.node.update` | 按 ID 更新节点 |
| `core.node.get` | 按数字 ID 获取节点 |
| `core.node.query` | 使用筛选器搜索/列出节点;返回 `{nodes, total}` |
| `core.node.delete` | **永久** 删除节点(使用 `update` with `status='draft'` 以取消发布而无需删除) |
| `core.nodetype.create` | 注册自定义帖子类型 |
| `core.nodetype.list` / `.get` / `.update` / `.delete` | 管理节点类型定义 |
| `core.render.node_preview` | 预览渲染的页面 HTML(无事件,无视图计数) |
| `core.render.block` / `.layout` | 在隔离状态下测试块或布局 |
### 分类法和术语
| 工具 | 描述 |
|------|-------------|
| `core.taxonomy.create` / `.list` / `.get` / `.update` / `.delete` | 管理分类法定义 |
| `core.term.create` / `.list` / `.get` / `.update` / `.delete` | 管理分类法术语行 |
### 菜单、设置、媒体、文件
| 工具 | 描述 |
|------|-------------|
| `core.menu.create` / `.list` / `.get` / `.update` / `.delete` / `.upsert` | 管理菜单;`upsert` 是幂等的,并将 `page:"<slug>"` 项解析为 NodeID |
| `core.settings.get` / `.list` / `.set` | 站点设置 |
| `core.media.upload` / `.import_url` / `.get` / `.query` / `.delete` | 媒体库 |
| `core.files.store` / `.delete` | 原始文件存储(无数据库记录) |
### 主题和布局
| 工具 | 描述 |
|------|-------------|
| `core.theme.list` / `.active` / `.get` | 检查主题 |
| `core.theme.activate` / `.deactivate` | 热交换主题(无需应用重启) |
| `core.theme.deploy` | 将打包的主题(base64 ZIP)部署到 `themes/<slug>/` 并可选地激活 - 原子目录交换,无需 docker cp,无需 git push |
| `core.theme.rescan` | 重新扫描 `themes/` 并为磁盘上丢弃的任何目录插入行 |
| `core.layout.list` / `.get` / `.create` / `.update` / `.delete` / `.detach` / `.reattach` | 管理页面布局 |
| `core.block_types.list` / `.get` / `.create` / `.update` / `.delete` / `.detach` / `.reattach` | 管理内容块类型 |
| `core.field_types.list` | 列出内置字段类型及其 `how_to` 指南 |
### 扩展
| 工具 | 描述 |
|------|-------------|
| `core.extension.list` / `.get` | 检查扩展(活动/非活动) |
| `core.extension.activate` / `.deactivate` | 热激活/停用(无需应用重启;仅子进程) |
| `core.extension.deploy` | 将打包的扩展(base64 ZIP)部署到 `extensions/<slug>/` 并可选地热激活 - 原子目录交换,插件二进制文件 chmod'd,无需 docker cp,无需 git push |
| `core.extension.rescan` | 重新扫描 `extensions/` 并为磁盘上丢弃的任何目录插入行 |
### 用户、数据、管道
| 工具 | 描述 |
|------|-------------|
| `core.user.get` / `.query` | 只读用户查找 |
| `core.data.get` / `.query` / `.create` / `.update` / `.delete` | 低级表访问(优先使用类型化工具) |
| `core.email.send` | 通过活动电子邮件提供程序扩展发送 |
| `core.http.fetch` | 外发 HTTP(能力门控) |
| `core.event.emit` | 将自定义事件发出到总线 |
| `core.filter.apply` | 对值运行已注册的过滤器链 |
### 元数据
| 工具 | 描述 |
|------|-------------|
| `core.guide` | 决策树 + 配方 + 实时 CMS 状态快照 |
| `core.theme.standards` | 主题创作标准(规则 1.5/1.6 等) |
| `core.extension.standards` | 扩展创作标准(清单、功能、热部署) |
### AI 工作流示例
**从头创建旅行预订站点:**
```
1. core.theme.list → 查找主题 ID
2. core.theme.activate(id) → 激活主题,播种节点类型 + 内容
3. core.node.query(node_type="trip") → 验证已创建行程
4. core.render.node_preview(id=<trip_id>) → 检查渲染
```
**添加新的内容类型:```
1. core.nodetype.create(slug="recipe", label="Recipe", field_schema=[...])
2. core.node.create(node_type="recipe", title="Pho Bo", fields_data={...})
3. core.render.node_preview(id=<recipe_id>) → 验证
```
**部署在主存储库外部构建的主题:**
```
1. (本地)zip -r mytheme.zip mytheme/ # 包含 theme.json
2. core.theme.deploy(body_base64=<base64(mytheme.zip)>, activate=true)
3. core.render.node_preview(id=<home_node>) → 验证新外观
```
**部署在主存储库外部构建的扩展:**
```
1. (本地)构建主机 OS/架构的 gRPC 插件(例如 linux/amd64)
2. (本地)zip -r myext.zip myext/ # extension.json + bin/myext + admin-ui/dist
3. core.extension.deploy(body_base64=<base64(myext.zip)>, activate=true)
4. core.extension.get(slug="myext") → 确认 is_active=true
```
## 文件结构
```
cmd/squilla/ 应用程序入口点
internal/ 核心内核
coreapi/ CoreAPI 接口 + 适配器(Tengo,gRPC,内部)
cms/ 内容服务,主题加载器,扩展加载器
scripting/ Tengo VM 运行时
models/ GORM 模型
auth/ 会话身份验证,RBAC
events/ 事件总线
mcp/ MCP 工具服务器(~75 个工具,涵盖 17 个领域)
extensions/ 功能扩展(参见 extensions/README.md)
themes/ 主题仓库
admin-ui/ React SPA shell
proto/ 协议缓冲区定义
storage/ 本地文件存储
```
## 主题开发
主题是自包含的包:布局、部分、块、资产、脚本和页面模板。
### 主题结构```
themes/my-theme/
theme.json 清单(布局、块、资产、模板)
layouts/ 页面布局(default.html、trip.html 等)
partials/ 可重用片段(site-header.html、site-footer.html)
blocks/ 内容块(my-hero/view.html + block.json)
assets/ 静态文件(images/、styles/、scripts/)
scripts/ Tengo 脚本(主题.tengo 入口点)
templates/ 预填充的页面 JSON 文件
```
### 激活时发生什么
当调用 `core.theme.activate(id)` 或 `POST /admin/api/themes/:id/activate` 时:
1. **注销以前的主题** - 布局、块、部分孤立(不删除)
2. **theme.deactivated 事件** - 扩展(例如 media-manager)清除旧主题资产
3. **注册新主题** - 布局、块、部分、模板插入数据库
4. **执行 theme.tengo** - 注册节点类型、分类法、内容、事件处理程序、过滤器
5. **theme.activated 事件** - 扩展导入新主题的资产
6. **无需服务器重启**
### 关键一致性
#1 主题错误的原因是种子数据形状与模板访问模式不匹配。
**规则:** 种子脚本(theme.tengo)定义数据契约。模板必须匹配。
示例错误:种子存储 `tag` 为字符串,模板访问 `tag.name`:```
// theme.tengo 种子: tag: "Foodie`
// 模板使用: {{ with $fd.tag }}{{ .name }}{{ end }} ← CRASH
// 修复: {{ with $fd.tag }}{{ . }}{{ end }} ← 正确
```
### 模板上下文:`.fields` vs `.fields_data`
不同的模板上下文使用不同的键名访问节点字段:
| 处于何处 | 访问模式 | 示例 |
|---------------|---------------|---------|
| 布局模板(当前节点) | `.node.fields` | `{{ .node.fields.color }}` |
| 块模板(块字段) | `.fields` 或直接访问 | `{{ .heading }}` |
| Tengo 过滤器结果(list_nodes) | `.fields_data` | `{{ .fields_data.color }}` |
### 另见
- `docs/theming.md` - 完整的主题指南(1400+ 行)
- `docs/scripting_api.md` - Tengo 脚本参考
- `extensions/README.md` - 扩展指南(替代入口到 `docs/extension_api.md`)
- `docs/extension_api.md` - 构建扩展(清单、gRPC 插件、功能)
- `docs/theming.md` - 完整的主题指南(布局、部分、块、资产)
- `docs/forms.md` - 扩展公共 API 参考
- `docs/vdus.md` - 服务器驱动的 UI(divisions, categories, tags)
- `docs/core_dev_guide.md` - 内核开发工作流
- `docs/core_features.md` - 详尽的功能清单
- `docs/database-schema.md` - 数据库模式参考(27 个 GORM 模型,38 个迁移)
- `docs/security.md` - 安全姿态和 PR 时间检查清单
- `extensions/README.md` - 扩展指南(替代入口到 `docs/extension_api.md`)
- `themes/README.md` - 主题指南(布局、块、部分、Tengo 种子、表单连接)
连接信息
猜你喜欢
暂无推荐服务器