编写应用(插件)
应用从内部为 Antimatter 添加功能:一个用 Go 编写的服务器部分,用于钩入服务器;以及一个用 React 编写的 Web 应用部分,用于添加菜单、面板、消息类型和页面。本页以捆绑的投票应用为例,展示应用的结构,以及 Antimatter 的 Fusion 界面中有哪些变化。
应用的工作原理
- 服务器部分是一个 Go 程序。服务器将其作为独立进程启动,并通过 RPC 与其通信:它调用应用的钩子(例如消息即将发布、命令已执行……),而应用则调用服务器的插件 API(例如创建帖子、读取用户、存储数据……)。它还可以在
/plugins/<app id>/下提供 HTTP 路由。 - Web 应用部分是一个 JavaScript 捆绑包,随 Web 应用一起加载到浏览器中。它通过注册表注册 React 组件和回调:例如菜单中的按钮、面板,或绘制某种消息类型的方式。
- 这两部分都是可选的。应用以
.tar.gz捆绑包的形式安装,系统管理员可以启用、禁用和配置它。
Antimatter 可以运行专为 Mattermost 编写的应用,反之亦然,只要应用仅使用两者都支持的钩子即可。参见Antimatter 独有的钩子。
捆绑包结构
没有单独的起始模板:请从一个小型捆绑应用开始,例如antimatter-plugin-polls或antimatter-plugin-todo,并修改其 ID 和代码。投票应用的结构如下:
antimatter-plugin-polls/
├── plugin.json the manifest
├── Makefile build, test, bundle, deploy
├── build/ the manifest and pluginctl helper tools (Go), setup.mk
├── assets/icon.svg files shipped with the bundle
├── go.mod
├── server/
│ ├── main.go plugin.ClientMain(&Plugin{})
│ ├── plugin.go the Plugin type, OnActivate, ServeHTTP
│ ├── manifest.go generated from plugin.json
│ ├── api.go, command.go, hooks.go, store.go …
│ └── dist/ compiled binaries, one per platform
└── webapp/
├── package.json, webpack.config.js, tsconfig.json
├── i18n/ en.json and one file per language
├── src/index.tsx registers the app with the web app
├── src/manifest.ts generated from plugin.json
├── src/translations.ts
└── dist/main.js the bundle
可选:public/ 文件夹,原样提供在 /plugins/<app id>/public/ 下。
plugin.json
{
"id": "com.example.weather",
"name": "Weather",
"description": "Forecasts in the chat.",
"homepage_url": "https://github.com/example/weather",
"support_url": "https://github.com/example/weather/issues",
"icon_path": "assets/icon.svg",
"version": "1.0.0",
"min_server_version": "12.0.0",
"server": {
"executables": {
"linux-amd64": "server/dist/plugin-linux-amd64",
"linux-arm64": "server/dist/plugin-linux-arm64",
"darwin-amd64": "server/dist/plugin-darwin-amd64",
"darwin-arm64": "server/dist/plugin-darwin-arm64",
"windows-amd64": "server/dist/plugin-windows-amd64.exe"
}
},
"webapp": {"bundle_path": "webapp/dist/main.js"},
"settings_schema": {"settings": []}
}
id:唯一且永久;通常使用反向域名。它用于命名路由、存储的设置和 WebSocket 事件。version:语义化版本。捆绑应用将其从plugin.json中省略;其构建工具从最新的v*git 标签中获取版本。min_server_version:Antimatter 报告为12.0.0。settings_schema:系统控制台中的应用设置页面(参见设置)。
make apply 会将清单复制到 server/manifest.go 和 webapp/src/manifest.ts;make manifest-check 用于验证清单。
服务端部分
服务端部分从 Go 模块代理导入与 Mattermost 应用相同的 Go 模块 github.com/mattermost/mattermost/server/public。其 main 函数仅用于启动应用:
package main
import "github.com/mattermost/mattermost/server/public/plugin"
func main() {
plugin.ClientMain(&Plugin{})
}
Plugin 类型嵌入了 plugin.MattermostPlugin,从而获得 p.API,并实现了所需的钩子:
type Plugin struct {
plugin.MattermostPlugin
router *mux.Router
}
func (p *Plugin) OnActivate() error {
p.router = p.newRouter()
return p.API.RegisterCommand(&model.Command{
Trigger: "weather",
AutoComplete: true,
AutoCompleteDesc: "Show the forecast",
AutoCompleteHint: "[city]",
})
}
func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
city := strings.TrimSpace(strings.TrimPrefix(args.Command, "/weather"))
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: "Forecast for " + city + ": sunny",
}, nil
}
func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
p.router.ServeHTTP(w, r)
}
常用钩子
| 钩子 | 执行时机 |
|---|---|
OnActivate, OnDeactivate | 当应用被启用或禁用,以及服务器启动和停止时。请在 OnActivate 中注册命令和机器人。 |
OnConfigurationChange | 当设置更改时;使用 p.API.LoadPluginConfiguration(&cfg) 加载设置。 |
ServeHTTP | 对 /plugins/<app id>/… 的请求。当请求来自已登录用户时,服务器会设置 Mattermost-User-Id 请求头;除非请求旨在公开,否则应拒绝没有该请求头的请求。 |
ExecuteCommand | 应用注册的命令被执行时。 |
MessageWillBePosted, MessageWillBeUpdated | 在消息保存之前:返回修改后的帖子,或拒绝保存的原因。 |
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeleted | 事后执行,用于处理副作用。 |
ReactionHasBeenAdded, ReactionHasBeenRemoved | 表情回应。 |
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreated | 成员资格和频道变更。团队即社区。 |
FileWillBeUploaded | 在文件存储之前:修改文件或拒绝存储。 |
OnPluginClusterEvent | 来自集群中另一台服务器上同一应用的消息。 |
完整的列表及其确切签名位于 server/public/plugin/hooks.go 中;API 位于 server/public/plugin/api.go 中。pluginapi 包(server/public/pluginapi)将 API 封装为更友好的辅助函数。
有用的 API 调用
CreatePost、UpdatePost、SendEphemeralPost:以应用机器人或用户身份发布帖子。EnsureBotUser(&model.Bot{Username, DisplayName}):一次性创建应用机器人并获取其 ID。PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}):向 Web 应用发送事件。该事件到达时名称为custom_<app id>_poll_updated。OpenInteractiveDialog:打开对话框。LogInfo、LogError及相关方法:写入服务器日志,并标记应用的 ID。GetUser、GetChannel、HasPermissionToChannel:在代表用户执行操作之前,检查谁有权执行哪些操作。
应用程序会在集群中的每台服务器上运行。请将状态保存在键值存储或数据库中,而不仅仅保存在内存中。
AI 服务
Antimatter 的服务器会通过管理员设置的模型连接(参见管理员指南中的 AI)为应用调用 AI 模型。应用本身从不持有提供商的密钥:它只需指明代表哪位用户以及在何处操作,服务器便会检查该用户是否有权在此处使用该连接,应用其限制和预算,将令牌消耗计入该用户和应用,并审计该调用(不含内容)。
| 方法 | 功能说明 |
|---|---|
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest) | 聊天补全:消息、工具、结构化输出(JSONSchema)。 |
AIChatStream(…) | 相同的流式处理:读取流的 Events 直到通道关闭,或调用 Close 停止调用。 |
AIEmbed(…, *ai.EmbedRequest) | 文本嵌入。 |
AITranscribe(…, *ai.TranscribeRequest) | 语音转文本。 |
AIDecide(…, *ai.DecideRequest) | 向决策模型提出类型化问题(是或否、选择、评分),针对特定文本。 |
AIConnectionsForUser(userID, channelID, teamID) | 用户在此处可使用的连接,包含其模型和功能,但不包含密钥。 |
每个请求的 ConnectionID 用于选择连接,Model 用于选择该连接下的某个模型(留空则使用连接的默认模型)。Operation 是用于使用记录和审计记录的简短标签,绝不应包含内容。这些功能所需的最低服务器版本为 11.11。
actingUserID:例如发往您 ServeHTTP 的请求中的 Mattermost-User-Id 头,或钩子上下文中的用户(如帖子作者、执行命令的用户)。切勿从消息、Webhook 负载、URL 或其他应用的请求中获取:否则您的应用将消耗他人的访问权限。这些方法和 ai 包(server/public/ai)仅存在于 Antimatter 的模块副本中,而不在 Mattermost 发布的版本中。要使用它们,请在 go.mod 中添加 replace 行,针对 Antimatter 的 server/public 进行构建,例如 replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public。此类应用仅在 Antimatter 上运行。
数据存储
每个应用程序都在服务器数据库中拥有自己的键值存储:
KVSet(key, value)、KVGet(key)、KVDelete(key):值为字节,通常是 JSON。键最多 150 个字符。KVSetWithExpiry(key, value, seconds):值在该时间后消失。KVCompareAndSet(key, old, new)以及带有Atomic的KVSetWithOptions:当多个请求或服务器写入同一个键时,可安全更新。投票(Polls)以此方式保存每一票,并在发生冲突时重试。KVList(page, perPage):按页返回键。
Web 应用部分
该 bundle 通过 window.registerPlugin 注册一个类。其 initialize 方法接收注册表和 Redux store:
import manifest from './manifest';
class Plugin {
initialize(registry, store) {
registry.registerTranslations((locale) => translationsFor(locale));
registry.registerPostTypeComponent('custom_poll', PollPost);
registry.registerWebSocketEventHandler(`custom_${manifest.id}_poll_updated`, (msg) => {
store.dispatch(pollUpdated(JSON.parse(msg.data.poll)));
});
registry.registerReconnectHandler(() => store.dispatch(reloadPolls()));
}
}
window.registerPlugin(manifest.id, new Plugin());
- 共享库。Web 应用提供 React、ReactDOM、Redux、React Redux 和 React Intl:请将它们列为 webpack
externals(React、ReactDOM、Redux、ReactRedux、ReactIntl),而不是打包自己的副本。参见 Polls 的webpack.config.js。 - 调用你的服务器部分。使用
Client4.getOptions()(来自mattermost-redux/client)中的选项,获取${window.basename}/plugins/<app id>/…:这些选项携带服务器接受来自浏览器请求所需的请求头。 - 你自己的状态。
registerReducer添加一个 reducer,存储在state['plugins-<app id>']下。 - 类型。Antimatter 的额外注册表方法未包含在 Mattermost 发布的类型中:Polls 在
webapp/src/types/host.ts中声明了它使用的注册表。
注册表的方法及其参数在代码中有文档说明,位于 webapp/fusion/src/plugins/registry.ts(Fusion)和 webapp/channels/src/plugins/registry.ts(经典版)。
支持 Fusion 界面
Antimatter 有两个 Web 界面:Fusion(默认)和经典版。两者加载相同的 app bundle 并具有相同的注册表,但 Fusion 自行绘制许多内容,并以自己的方式放置插件组件。请在两种界面中测试你的应用。
检测界面
在任何应用加载之前,两种界面都会将 window.antimatterWebUI 设置为 'fusion' 或 'classic',并在页面根元素上以 data-am-web-ui 设置相同的值。在 Mattermost 上,两者均未设置。
export function isFusionUI(): boolean {
return window.antimatterWebUI === 'fusion' || document.documentElement.dataset.amWebUi === 'fusion';
}
内置应用使用此功能以不同的方式绘制界面。投票应用会以 Fusion 的风格绘制其投票卡片和创建对话框,并在 Fusion 中为 /poll 打开其专属对话框。通话和语音频道会省略侧栏和提示组件,因为 Fusion 会用其自身的通话界面替代它们。智能体应用将其 AI 操作放入消息菜单中,作为 更多操作 下的一个分组,而不是悬浮工具栏。Fusion 自身的 CSS 类(以 am- 为前缀)及其对话框层(am-layer 元素)是这些应用用来匹配界面的方式;它们并非文档化的稳定 API。
Fusion 显示插件组件的位置
| 注册表方法 | 在 Fusion 中的显示位置 |
|---|---|
registerAppBarComponent, registerChannelHeaderButtonAction | 应用栏是位于右侧边缘、介于您的头像和关于 Antimatter按钮之间的一列图标。频道标题按钮也位于此处,而非频道标题中。当系统管理员启用禁用应用栏(ExperimentalSettings.DisableAppBar,默认关闭)时,该栏将隐藏。 |
registerRightHandSidebarComponent | 右侧面板。用户可以通过拖动边缘来调整其大小,也可以在新窗口中打开它。如果该应用也是一个产品,面板会提供 作为完整页面打开。参见 在新窗口中的面板。 |
registerChannelHeaderIcon | 频道标题中的图标,位于通话按钮之后;在手机上,位于标题的 更多 菜单中。 |
registerCallButtonAction | 频道标题中的通话按钮;多个提供商共享一个 其他通话方式 菜单。 |
registerPostDropdownMenuAction | 消息的 消息操作(⋯)菜单,位于其 更多操作 子菜单中;也位于话题面板的 话题操作 菜单中,位于 更多操作 下。Fusion 会在每个操作前绘制应用图标,颜色为灰色:对于内置应用,使用其自身图标;否则使用应用显示在应用栏中的图标;否则使用中性的图标。如果操作文本开头没有文本元素(即应用自行绘制的图标),则该元素会被隐藏。 |
registerPostDropdownSubMenuAction | 在同一个 更多操作 子菜单中,位于单个操作之后的一个带标题的分组。没有应用会在消息菜单中获得自己的独立条目。 |
registerPostActionComponent | 消息悬停工具栏,位于 ⋯ 按钮之前。 |
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponent | 消息本身:其正文,位于文本下方、时间之后,以及链接预览的位置。 |
registerChannelHeaderMenuAction | 频道的 频道选项 菜单(侧栏中频道的 ⋯,或右键点击),作为带有插头图标的项显示。 |
registerMainMenuAction | 社区菜单(频道列表顶部的社区名称),位于 更多操作 下,按应用程序分组。 |
registerSidebarBrowseOrAddChannelMenuAction | 社区菜单,位于 创建频道 和 创建分类 之后。 |
registerFileUploadMethod | 编辑器的 + 菜单(添加到消息),位于 上传文件 之后。 |
registerPostEditorActionComponent | 编辑器的格式栏,当显示格式选项时。 |
registerAIActionMenuItemComponent | 编辑器的 AI 菜单。 |
registerFileDropdownMenuAction, registerFilePreviewComponent | 文件的选项菜单,以及文件预览。 |
registerUserSettings | 个人资料与设置,在 插件 标题下,每个应用程序一个选项卡。 |
registerChannelSettingsTab | 频道设置对话框,位于 应用程序 标题下。 |
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponent | 点击名称或头像时显示的个人资料卡片。 |
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponent | 社区频道列表的顶部、频道名称旁、频道图标旁,以及左侧社区栏的底部。 |
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholder | 频道历史的起始位置、消息列表的顶部、未读分隔线、输入框上方,以及输入框的占位符。 |
registerChannelTypeOption | 创建频道对话框中的频道类型。 |
registerSearchComponents | 频道标题栏中的搜索框:当搜索框为空时,会显示应用范围。 |
registerUserGuideDropdownMenuAction | 关于 Antimatter(位于应用栏底部),位于 帮助 之下。 |
registerProduct | 位于产品 URL 下的完整页面,列在 关于 Antimatter 中的 产品 下。产品页面保留经典页面框架,其产品菜单仅在应用注册产品或向其添加条目时显示。内置的看板和操作手册应用不再注册产品:它们的页面在其侧面板中打开。 |
registerRootComponent、registerGlobalComponent,以及诸如 registerMessageWillBePostedHook、registerSlashCommandWillBePostedHook、registerMessageWillFormatHook、registerFilesWillUploadHook、registerDesktopNotificationHook、registerWebSocketEventHandler、registerReducer、registerTranslations 等钩子。 | 与经典界面中的行为一致。 |
registerAdminConsolePlugin, registerAdminConsoleCustomSetting | 系统控制台,在两种界面中均为经典版本。 |
Fusion 不显示或不运行的内容
registerPostDropdownMenuComponent(消息菜单中的组件)、registerPostCardTypeComponent(右侧面板中的帖子卡片)和registerProductSwitcherMenuItem不会显示在 Fusion 的主视图中。请改用registerPostDropdownMenuAction替代前者。- 在 Fusion 中编辑消息时,
registerMessageWillBeUpdatedHook不会运行。请改用MessageWillBeUpdated钩子在服务器端检查编辑操作。 registerActionAfterChannelCreation仅在经典频道创建对话框中运行,Fusion 会从 创建频道 对话框中的 更多选项… 打开该对话框。- 无论
supportedProductIds如何,应用栏组件都会显示。 - 来自
registerNeedsTeamRoute和registerCustomRoute的路由保留经典页面框架,与产品页面类似。
Antimatter 独有的钩子
这些注册方法在 Mattermost 中不存在。调用前请检查方法是否存在,以确保应用也能在该环境中正常工作:
if (typeof registry.registerComposerMenuItem === 'function') {
registry.registerComposerMenuItem({
text: 'Create a poll',
icon: <i className='icon icon-chart-bar'/>,
fusionIcon: 'poll',
action: ({channelId, rootId}) => openPollCreation(channelId, rootId),
});
} else {
registry.registerChannelHeaderButtonAction(icon, (channel) => openPollCreation(channel.id), 'Create a poll', 'Create a poll');
}
| 方法 | 添加的内容 |
|---|---|
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender}) | 在两种界面中,消息框 + 菜单中的一项。action 接收 {channelId, rootId}。 |
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker}) | 消息框表情选择器的一个标签页,例如 GIF 或贴纸。在 Fusion 中,order 低于 10 的标签页会排在表情符号标签页之前。该组件接收 channelId、rootId、filter、onFilterChange、onSelectDone、insertText、isFusion 以及,在 Fusion 中,pickerKit(见下文)。 |
registerChannelViewPanel({matcher, component}) | 位于频道标题和消息之间的面板,用于 matcher(state, channel) 选中的频道。它可以隐藏消息和输入框(setMessagesVisible),语音频道就是这样做的。 |
registerSidebarChannelFooterComponent(component) | 侧栏中频道下方的一行,例如显示该频道通话中的人员。它接收 channel。 |
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender}) | 仅限 Fusion:集成页面上的一个条目,位于自定义表情符号旁边。GIF 应用会在那里添加其自定义 GIF 和贴纸。 |
在表情选择器中添加和管理视图
在 Fusion 中,表情选择器标签页会接收一个 pickerKit 属性:该属性包含表情标签页用于其自身添加和管理视图的组件,从而确保标签页的视图外观一致,并保持在选择器内部。GIF 和表情包应用使用它来添加和管理 GIF、贴纸和表情包模板。它包含 version(1)、钩子 usePickerView 和 useOnEscape,以及组件 PickerViewButtons(搜索框旁边的 + 和管理按钮)、PickerViewFrame(带有返回箭头的视图)、PickerNotice、PickerField、PickerSearch、PickerDropZone 和 PickerManageList。按下 Escape 键会从当前视图返回到标签页的网格。请检查该属性是否存在:其他宿主环境不包含此属性,此时标签页将保留其自身的对话框。类型定义位于 webapp/fusion/src/types/store/plugins.ts(EmojiPickerTabProps),组件位于 webapp/fusion/src/fusion/popovers/picker_views.tsx。
在新窗口中的面板
Fusion 为 registerRightHandSidebarComponent 的组件提供了另外两个属性,以便 在新窗口中打开 时,面板能打开在其原本所在的位置,而不是应用的主页:
setPanelState(state):当面板位置发生变化时调用此方法,传入一个由你的应用定义的短字符串,例如'note/<id>'或'board/<id>/card/<id>',或者在其主页时传入null。该状态会写入窗口地址栏,因此切勿在其中包含敏感信息。长度超过 512 个字符或包含控制字符的状态将被丢弃。initialPanelState:在新窗口中,这是面板之前的状态,或者为undefined。请将其视为不可信输入:以当前登录用户的身份重新获取该条目,如果用户无法打开该条目,则显示应用主页或“未找到”视图。
当面板关闭或被其他应用的面板取代时,该状态将被遗忘。若要自行打开面板窗口,popoutRhsPlugin(也位于 window.WebappUtils.popouts 中)接受状态作为可选的第五个参数。笔记、白板、待办事项、看板和操作手册均使用此功能。
fusionIcon 指定 Fusion 图标集中的某个图标(例如 poll、sticker、calendar 或 image;参见 webapp/fusion/src/fusion/components/icon)。Fusion 会显示它而不是 icon,从而使该条目与其他条目保持一致。
设置
plugin.json 中的 settings_schema 描述了该应用在系统控制台 › 插件中的页面:
"settings_schema": {
"header": "Forecasts come from the weather service below.",
"settings": [
{"key": "ServiceURL", "display_name": "Service URL", "type": "text", "default": "https://api.example.com"},
{"key": "Units", "display_name": "Units", "type": "dropdown", "default": "metric",
"options": [{"display_name": "Celsius", "value": "metric"}, {"display_name": "Fahrenheit", "value": "imperial"}]},
{"key": "EnableAlerts", "display_name": "Severe weather alerts", "type": "bool", "default": false,
"help_text": "Post alerts in the channels that follow a city."}
]
}
设置类型:bool、text、longtext、number、dropdown、radio、generated(随机密钥)、username 和 custom(由 Web 应用部分通过 registerAdminConsoleCustomSetting 注册的组件绘制)。设置可以分组到 sections 中。
服务器部分将它们读取到一个结构体中,其字段与键匹配:
type configuration struct {
ServiceURL string
Units string
EnableAlerts bool
}
func (p *Plugin) OnConfigurationChange() error {
var cfg configuration
if err := p.API.LoadPluginConfiguration(&cfg); err != nil {
return err
}
p.setConfiguration(&cfg)
return nil
}
管理员也可以在配置中设置它们,位于 PluginSettings.Plugins.<app id> 下,键使用小写字母(参见管理员指南)。
翻译
Antimatter 已翻译成 37 种语言,捆绑的应用程序也是如此。Web 应用部分是这样做的:
- 使用 React Intl 编写每个字符串,包含 ID 及其英文文本:
<FormattedMessage id='polls.create' defaultMessage='Create a poll'/>或defineMessages。ID 前缀使用您的应用名称。 - 将英文字符串提取到
webapp/i18n/en.json(捆绑的应用程序使用formatjs extract)。 - 在其旁边为每种语言添加一个文件,使用相同的 ID:
fr.json、de.json、pt-BR.json、zh-CN.json… - 将它们传递给 Web 应用,该应用会询问用户的语言:
import fr from '../i18n/fr.json'; import ptBR from '../i18n/pt-BR.json'; const catalogs = {fr, 'pt-BR': ptBR}; export function translationsFor(locale) { return catalogs[locale] || catalogs[locale.split('-')[0]] || {}; } registry.registerTranslations(translationsFor);
缺失的字符串会回退到其英文 defaultMessage。对于由服务器部分发送的字符串(命令响应、机器人消息),pluginapi/i18n 包会加载捆绑包中附带的翻译文件(例如 assets/i18n),并为每位用户提供相应语言的本地化器。
构建、安装和调试
你需要 Go 和 Node.js(版本见应用目录中的 .nvmrc)。在应用目录中:
| 命令 | 功能说明 |
|---|---|
make dist | 为所有平台构建服务器部分和 Web 应用部分,并将捆绑包写入 dist/<app id>-<version>.tar.gz。 |
make test | 运行 Go 和 JavaScript 测试。 |
make check-style | 检查代码(golangci-lint、ESLint)并验证 TypeScript 类型。 |
make deploy | 构建后,在服务器上安装并启用该捆绑包。 |
make watch, make deploy-from-watch | 第一个命令在你每次保存时都会重新构建 Web 应用部分;第二个命令将构建结果打包并安装到服务器上。 |
make enable, make disable, make reset | 在服务器上启用、禁用或重启该应用。 |
make logs, make logs-watch | 显示服务器日志中与该应用相关的行。 |
make attach | 将 Delve 调试器附加到正在运行的服务器部分(请先使用 AM_DEBUG=1 进行构建)。 |
make deploy 和其他服务器命令在检测到本地模式套接字(/var/tmp/antimatter_local.socket,或 AM_LOCALSOCKETPATH)时会使用它:请在服务器的机器上以本地模式运行这些命令。否则,它们会使用 AM_ADMIN_TOKEN,或 AM_ADMIN_USERNAME 和 AM_ADMIN_PASSWORD 登录到 MM_SERVICESETTINGS_SITEURL 指定的服务器。构建工具目前尚不支持通过 AM_ 前缀的变量名来读取站点 URL。
手动安装捆绑包
- 系统管理员可通过
AM_PLUGINSETTINGS_ENABLEUPLOADS=true允许上传(默认关闭;系统控制台中没有相应的开关)。 - 然后进入 系统控制台 › 插件 › 插件管理 › 上传插件,或者执行
amctl plugin add weather.tar.gz,接着执行amctl plugin enable com.example.weather。 - 如果服务器启用了需要插件签名,则仅接受使用受信任密钥签名的捆绑包;请参阅安装其他应用。
AM_SERVICESETTINGS_ENABLEDEVELOPER=true:此时错误信息将包含详细信息。捆绑包应用的 Makefile 仅当你的 shell 中设置了 MM_SERVICESETTINGS_ENABLEDEVELOPER 时,才为你的平台构建服务器部分。