编写应用(插件)

应用从内部为 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 应用部分是这样做的:

  1. 使用 React Intl 编写每个字符串,包含 ID 及其英文文本:<FormattedMessage id='polls.create' defaultMessage='Create a poll'/> 或 defineMessages。ID 前缀使用您的应用名称。
  2. 将英文字符串提取到 webapp/i18n/en.json(捆绑的应用程序使用 formatjs extract)。
  3. 在其旁边为每种语言添加一个文件,使用相同的 ID:fr.json、de.json、pt-BR.json、zh-CN.json…
  4. 将它们传递给 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 时,才为你的平台构建服务器部分。