app

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Overview

Package app 定义模块契约与应用注册器。

v1 不做运行时插件加载,但所有功能模块必须以「编译期插件」形态组织: 核心不得直接依赖模块内部实现。这样 v2 接入 WASM 或 gRPC 宿主时,核心无需重写。

Index

Constants

View Source
const (
	NavGroupDashboard  = "dashboard"
	NavGroupContent    = "content"
	NavGroupMedia      = "media"
	NavGroupAppearance = "appearance"
	NavGroupUsers      = "users"
	NavGroupSettings   = "settings"
	NavGroupSystem     = "system"
)

Console 侧边栏的分组标识(七组页面地图)。

分组定义在核心而不是各模块各写一份:分组是**版面的骨架**, 谁往里放东西是模块的事,骨架长什么样只该有一处说了算。 插件要新开一组时自己声明一个 NavGroup,名称不冲突即可。

Variables

This section is empty.

Functions

func ApplyFilter added in v0.2.0

func ApplyFilter[T any](ctx context.Context, e Events, filter string, value T) T

ApplyFilter 是 Filter 的类型化包装:没人订阅时原样返回;插件的结果解不回 T 时也原样返回。

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App 是模块注册与共享依赖的中心。

模块只通过 App 获取依赖并注册能力,不直接互相引用——这是编译期插件形态 的关键约束,也是 v2 换成进程外插件时唯一需要替换的接缝。

生命周期:New → Register(装配)→ 迁移 → Start(播种、后台任务)→ 对外服务 → Close。

func New

func New(opts *Options) *App

New 构造 App。

func (*App) Close

func (a *App) Close(ctx context.Context) error

Close 逆序关闭实现了 Closer 的模块。

逆序是因为后注册的模块可能依赖先注册者;错误全部收集后一并返回, 不因单个模块失败而跳过其余模块的清理。

func (*App) Config

func (a *App) Config() config.Config

Config 返回运行配置。

func (*App) DB

func (a *App) DB() *database.DB

DB 返回数据库客户端,可能为 nil。

func (*App) Events added in v0.2.0

func (a *App) Events() Events

Events 返回事件总线,总是非 nil;模块可以在装配期就拿住它,实现晚些接上也无妨。

func (*App) Frontend added in v0.2.0

func (a *App) Frontend() Frontend

Frontend 返回前台扩展入口,总是非 nil。

func (*App) Hooks

func (a *App) Hooks() []Hook

Hooks 返回按优先级升序排列的钩子。

func (*App) Logger

func (a *App) Logger() *slog.Logger

Logger 返回应用日志器。

func (*App) Lookup

func (a *App) Lookup(name string) (any, bool)

Lookup 取回 Provide 登记的共享服务;未登记时返回 false。

func (*App) Migrations

func (a *App) Migrations() []NamedFS

Migrations 返回所有模块贡献的迁移文件系统,顺序即注册顺序。

func (*App) Modules

func (a *App) Modules() []string

Modules 返回已注册模块的名称,顺序即注册顺序。

func (*App) Navigation

func (a *App) Navigation() Navigation

Navigation 汇总全部模块声明的菜单,按分组顺序与组内顺序排好。

每次调用都重新问一遍各模块,而不是在注册期收集它们的返回值: 设置分组要等 Start 时才登记(见 internal/settings), 而设置项对应的菜单入口正是从那份分组列表推导出来的。注册期收集会漏掉它们。

菜单是给人看的,顺序错了顶多难看;但标识别扭了会让前端静默丢项, 故对声明做一遍校验,把问题在测试与启动日志里说清楚。

func (*App) Permissions

func (a *App) Permissions() []Permission

Permissions 返回所有模块声明的权限串。

func (*App) Provide

func (a *App) Provide(name string, value any)

Provide 登记一个供其他模块使用的共享服务(如设置读取器)。

这是模块之间唯一的依赖注入点:先注册的模块在 Register 里 Provide, 后注册的模块经 Lookup 取用,依赖方向由 modules.go 的装配顺序决定。

func (*App) Register

func (a *App) Register(modules ...Module) error

Register 依次注册模块,并用类型断言收集可选能力。

任一模块注册失败即整体失败:核心基座不接受「部分可用」的启动状态。

func (*App) Router

func (a *App) Router() Router

Router 返回三平面注册面,可能为 nil(非 HTTP 场景)。

func (*App) SetEvents added in v0.2.0

func (a *App) SetEvents(e Events)

SetEvents 接上事件总线的实现,由插件模块在装配时调用。

func (*App) SetFrontend added in v0.2.0

func (a *App) SetFrontend(f Frontend)

SetFrontend 接上前台扩展的实现,由插件模块在装配时调用。

func (*App) Settings

func (a *App) Settings() []SettingGroup

Settings 返回所有模块注册的设置分组。

func (*App) Start

func (a *App) Start(ctx context.Context) error

Start 按注册顺序启动实现了 Starter 的模块。

必须在迁移完成之后调用:Start 是模块第一次被允许访问数据库的时机。 任一模块启动失败即整体失败。

type Closer

type Closer interface {
	Close(ctx context.Context) error
}

Closer 是需要在应用关闭时释放资源的模块可选能力。

type Events added in v0.2.0

type Events interface {
	// Emit 通知一个动作。实现须异步执行,不拖慢调用方,也不向调用方报错。
	Emit(ctx context.Context, action string, data any)
	// Subscribed 判断有没有插件订阅了某个过滤器;没有时调用方可以连编码都省掉。
	Subscribed(filter string) bool
	// Filter 把值依次交给订阅了过滤器的插件,返回最终的值;任何一环失败都跳过那一环。
	Filter(ctx context.Context, filter string, value json.RawMessage) json.RawMessage
}

Events 是内核向插件派发动作与过滤器的总线。

内核模块不认识插件:它们只在事情发生之后 Emit,在要把数据交给别人改一遍时 Filter。 插件模块装配时经 SetEvents 接上实现;没接的时候总线是空的——Emit 什么也不做, Filter 原样返回,内核照常工作。

type Frontend added in v0.2.0

type Frontend interface {
	// Slot 返回插件放进某个插槽的内容。
	Slot(ctx context.Context, slot string, page *hooks.Page) template.HTML
	// Widget 渲染一个插件小组件,id 形如 <插件>/<小组件>;插件没启用或没产出内容时 ok 为假。
	Widget(ctx context.Context, id string, page *hooks.Page) (label string, html template.HTML, ok bool)
	// Shortcodes 展开正文里的短代码,返回新的正文;没有可展开的短代码时 changed 为假。
	Shortcodes(ctx context.Context, html string, page *hooks.Page) (out string, changed bool)
	// StripShortcodes 去掉纯文字里认得的短代码,摘要与页面描述里用。
	StripShortcodes(text string) string
}

Frontend 是插件往前台页面里放东西的入口:插槽、侧栏小组件与短代码。

主题模块在渲染时调用它,插件模块是它的实现;返回的 HTML 已按插件的规则净化过, 主题原样放进页面。没接上实现时插槽为空、小组件不显示、短代码原样保留。

type Hook

type Hook struct {
	// Name 是钩子点标识。
	Name string
	// Priority 决定执行顺序,数值小者先执行。
	Priority int
	// Handler 是钩子实现。
	Handler HookFunc
}

Hook 是一条钩子声明。

type HookFunc

type HookFunc func(http.Handler) http.Handler

HookFunc 是钩子的处理函数,签名随阶段 3 的具体钩子点细化。

type HookProvider

type HookProvider interface {
	Hooks() []Hook
}

HookProvider 声明模块注册的钩子。

type Migrator

type Migrator interface {
	Migrations() fs.FS
}

Migrator 声明模块自带数据库迁移。

返回的文件系统根目录直接包含 goose SQL 文件;每个模块拥有独立的版本表, 迁移编号只需在模块内递增(见 internal/migrate)。

type Module

type Module interface {
	// Name 返回模块的唯一标识,用于日志、迁移版本表名与依赖诊断。
	// 须为小写字母开头的短标识(字母、数字、下划线、连字符)。
	Name() string
	// Register 把模块的能力挂载到应用上。
	//
	// 只做装配,不得访问数据库:migrate 命令也会走同一条注册链,而那时业务表
	// 可能尚不存在。需要读写库的启动逻辑放到 Starter.Start。
	Register(app *App) error
}

Module 是每个功能模块必须实现的最小接口。

type NamedFS

type NamedFS struct {
	Module string
	FS     fs.FS
}

NamedFS 是带来源模块名的迁移文件系统,便于错误定位与版本表命名。

type NavGroup struct {
	// Name 是分组标识(DNS-1123),菜单项用它指认自己属于哪一组。
	Name string
	// Label 是分组显示名。
	Label string
	// Order 决定分组顺序,数值小者在前。
	Order int
}

NavGroup 是 Console 侧边栏的一个分组。

分组由模块声明而不是前端写死:插件的页面要能出现在侧边栏里, 而它归哪一组只有插件自己知道。前端因此只负责把收到的东西画出来。

func NavGroups() []NavGroup

NavGroups 返回内置的七组,顺序即侧边栏自上而下的顺序。

用分组而非扁平列表,是为了插件的页面有确定的安放位置—— 届时插件只需声明自己属于哪一组,侧边栏不必改结构。

type NavItem struct {
	// Key 是菜单项的稳定标识,同一分组内唯一。供前端做 key 与诊断用。
	Key string
	// Label 是菜单项显示名。
	Label string
	// Path 是 Console 内的绝对路径,如 /posts。
	//
	// 必须是绝对路径:前端以 basename=/console 挂载 SPA,
	// 相对路径会被解析到当前页之下。
	Path string
	// Icon 是图标名,取值来自 console/src/lib/icons.ts 的登记表。
	//
	// 后端只能给出名字,而两侧之间没有编译期约束:写了登记表里没有的名字,
	// 前端只会画一个默认图标,界面上不会有任何提示。故由 cmd/lumo 的契约测试盯着。
	Icon string
	// Group 是所属分组的 Name。
	Group string
	// Order 决定同组内的顺序,数值小者在前。
	Order int
	// Permission 是显示所需的权限串;留空表示所有已登录用户可见。
	//
	// 这是**显示条件而非安全边界**:没有权限就不显示入口,真正的拦截在服务端。
	// 前端隐藏按钮只是少让人白跑一趟。
	Permission string
	// Keywords 是命令面板的检索关键词(含拼音),便于中文输入习惯。
	Keywords string
	// Description 是一句话说明,供命令面板与悬停提示使用;可留空。
	Description string
	// End 为真时只在路径完全相等时高亮,用于「概览」这类根路径项。
	End bool
	// Hidden 为真时不进侧边栏,但仍参与命令面板与标题映射(如个人中心)。
	Hidden bool
}

NavItem 是 Console 侧边栏的一个菜单项。

type Navigation struct {
	Groups []NavGroup
	Items  []NavItem
}

Navigation 是一批菜单声明。

func CoreNavigation

func CoreNavigation() Navigation

CoreNavigation 返回核心自身的菜单声明。

用户、角色、关于、日志这几页由核心实现,没有对应的功能模块,故在这里声明—— 与 app.CorePermissions 同一个理由。其余各页由各自的功能模块声明。

type NavigationProvider interface {
	Navigation() Navigation
}

NavigationProvider 声明模块贡献的 Console 菜单。

与 SettingsProvider 并列:模块有什么页面,就该由它自己说, 而不是让前端维护一份必须与后端能力保持同步的硬编码清单。

type Options

type Options struct {
	Config config.Config
	// DB 可为 nil:不需要数据库的命令(如仅渲染帮助)也应能构造 App。
	DB     *database.DB
	Logger *slog.Logger
	// Router 可为 nil:migrate 等非 HTTP 场景不注册接口。
	Router Router
}

Options 是构造 App 的依赖。

type Permission

type Permission struct {
	// Key 是权限串,如 posts:write。
	Key string
	// Label 是显示名,走 i18n key。
	Label string
	// Description 是权限说明。
	Description string
}

Permission 是一条权限声明,格式为 <资源>:<动作>。

func CorePermissions

func CorePermissions() []Permission

CorePermissions 返回核心自身引入的权限声明。

与模块的 Permissions() 并列:用户、角色、主题与站点级操作由核心实现, 没有对应的功能模块,故在这里声明。角色编辑器据此拿到中文名, 缺了它这些权限会以「有权限串、没名字」的形态出现在界面上。

type PermissionProvider

type PermissionProvider interface {
	Permissions() []Permission
}

PermissionProvider 声明模块引入的权限串。

type RouteProvider

type RouteProvider interface {
	Routes(r Router)
}

RouteProvider 声明模块挂载 HTTP 接口。

接口注册通过三平面注册面进行,模块不能直接接触根路由, 以保证鉴权与前缀约定不被绕过。

type Router

type Router interface {
	// Console 挂载 /api/v1/console/** :已解析凭据、CSRF 校验、强制已认证;
	// 细粒度权限由各操作用 auth.RequirePermission 或 Principal.Allows 声明。
	Console() huma.API
	// ConsolePublic 挂载 Console 平面下**免认证**的端点,仅供登录一类极少数场景。
	ConsolePublic() huma.API
	// Public 挂载 /api/v1/public/** :解析凭据但不强制,匿名可读已发布内容。
	Public() huma.API
	// Extension 挂载 /apis/{group}/{version}/{kind} :强制已认证。
	Extension() huma.API
	// Raw 在站点根路径上挂一个直接写响应的处理器,供 robots.txt、sitemap.xml、
	// 订阅源一类**非 JSON 且必须位于根路径**的资源使用。
	//
	// 这是三平面之外唯一的注册口子,刻意开得很窄:它不经过任何鉴权与内容协商,
	// 挂上去的处理器对匿名请求可见。业务接口一律走上面四个注册面。
	Raw(method, path string, handler http.HandlerFunc)
}

Router 是暴露给模块的接口注册面。

四个注册面都是 huma.API:模块用 huma.Register 声明操作,请求校验、错误格式与 OpenAPI 文档随之自动生成。前缀与鉴权中间件挂在各平面的分组上,模块无需也不能自行处理。

type SettingGroup

type SettingGroup struct {
	// Name 是分组标识(DNS-1123),如 site、seo、mail。
	Name string
	// Label 是分组显示名。
	Label string
	// Description 是分组说明。
	Description string
	// Order 决定 Console 中的显示顺序,数值小者在前。
	Order int
	// Icon 是该分组的图标名,取值来自 console/src/lib/icons.ts 的登记表。
	//
	// 分组自己声明图标,设置页的区块标题与命令面板入口因此都从分组列表推导——
	// 少一处需要手工同步的清单,也就不会再出现「后端有这一组、界面上没有入口」。
	Icon string
	// Toggle 是本组主开关的字段名,可留空。
	//
	// 设置页把所有分组铺在一页上,各组默认收起;声明了主开关的组,
	// 那个开关会提到区块标题栏上——「开放评论」「启用邮件发送」「开放注册」
	// 这类决定「这一整块要不要生效」的开关,是站长最常动的东西,
	// 让它需要先展开一层才够得着,就是把最常用的动作放到最深的地方。
	//
	// 字段必须存在于 Form 中且为布尔,否则注册时报错:写错一个名字的代价
	// 是标题栏上少个开关,而那在界面上没有任何提示。
	Toggle string
	// Form 是表单声明。模块用 internal/form 的 DSL 构建;
	// 主题包从 settings.yaml 读入后经 form.Build 得到。
	//
	// 值 = Form 的缺省值被已保存值按顶层键覆盖;Schema、缺省值与条件依赖
	// 三者同出一处,不再有「stuct 改了 Schema 忘了改」这种缝。
	Form *form.Form
	// Public 列出可经 Public 平面读取的字段名(如站点标题);其余字段仅 Console 可见。
	Public []string
	// Check 是 Schema 之外的 Go 侧校验(如时区名是否真实存在),入参为合并默认值后的有效值;可为 nil。
	Check func(values map[string]any) error
}

SettingGroup 是一组设置项声明,走统一表单 Schema。

分组是设置的读写单位:Console 按分组渲染表单,接口按分组整体替换值。

type SettingsProvider

type SettingsProvider interface {
	Settings() []SettingGroup
}

SettingsProvider 声明模块注册设置项分组,走统一表单 Schema。

type Starter

type Starter interface {
	Start(ctx context.Context) error
}

Starter 声明模块需要在迁移完成之后、对外服务之前执行启动逻辑, 如播种内置数据、启动后台任务。ctx 在应用关闭时取消,后台 goroutine 应据此退出。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL