Создание приложений (плагинов)
Приложения добавляют функции в Antimatter изнутри: серверная часть на Go, которая подключается к серверу, и веб-часть на React, которая добавляет меню, панели, типы сообщений и страницы. На этой странице показана структура приложения на примере встроенного приложения Polls, а также изменения в интерфейсе Fusion Antimatter.
Как работает приложение
- Серверная часть — это программа на Go. Сервер запускает её как отдельный процесс и взаимодействует с ней через RPC: он вызывает хуки приложения (сообщение вот-вот будет отправлено, команда была выполнена…), а приложение вызывает API плагина сервера (создать сообщение, прочитать пользователя, сохранить данные…). Приложение также может обслуживать HTTP-маршруты по адресу
/plugins/<app id>/. - Веб-часть — это JavaScript-пакет, загружаемый в браузер вместе с веб-приложением. Он регистрирует компоненты React и обратные вызовы в реестре: кнопку в меню, панель, способ отрисовки типа сообщения.
- Любая из частей является необязательной. Приложение устанавливается как пакет
.tar.gz, а системные администраторы активируют, деактивируют и настраивают его.
Antimatter запускает приложения, написанные для Mattermost, и наоборот, при условии, что приложение использует только хуки, доступные в обеих системах. См. Хуки, доступные только в Antimatter.
Структура пакета
Отдельного стартового шаблона нет: начните с небольшого встроенного приложения, такого как antimatter-plugin-polls или antimatter-plugin-todo, и измените его ID и код. Приложение Polls имеет следующую структуру:
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; их инструмент сборки получает её из последней git-меткиv*.min_server_version: Antimatter сообщает12.0.0.settings_schema: страница настроек приложения в Системной консоли (см. Параметры).
make apply копирует манифест в server/manifest.go и webapp/src/manifest.ts; make manifest-check проверяет его.
Серверная часть
Серверная часть импортирует тот же Go-модуль, что и приложения Mattermost, github.com/mattermost/mattermost/server/public, из прокси Go-модулей. Её 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}): отправляет событие веб-приложению. Оно приходит какcustom_<app id>_poll_updated.OpenInteractiveDialog: открывает диалог.LogInfo,LogErrorи другие: запись в журнал сервера с меткой ID приложения.GetUser,GetChannel,HasPermissionToChannel: проверка того, кто имеет право на какие действия, перед выполнением действий от имени пользователя.
Приложение выполняется на каждом сервере кластера. Храните состояние в хранилище «ключ-значение» или в базе данных, а не только в оперативной памяти.
Служба ИИ
Сервер Antimatter обращается к моделям ИИ для приложений через соединения с моделями, настроенные администратором (см. ИИ в руководстве администратора). Приложение никогда не хранит ключ провайдера: оно указывает, от чьего имени и где действует, а сервер проверяет, что этот человек может использовать соединение в этом месте, применяет его ограничения и бюджеты, учитывает токены за человека и приложение, а также ведет журнал вызовов без их содержимого.
| Метод | Назначение |
|---|---|
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 только из источника, проверенного сервером: заголовок Mattermost-User-Id запроса к вашему ServeHTTP или пользователь из контекста хука (автор сообщения, пользователь, выполнивший команду). Никогда не берите его из сообщения, полезной нагрузки вебхука, URL или запроса другого приложения: иначе ваше приложение будет расходовать чужой доступ.Эти методы и пакет ai (server/public/ai) существуют только в копии модуля Antimatter, а не в версии, опубликованной Mattermost. Чтобы использовать их, собирайте проект против server/public Antimatter, добавив строку replace в ваш go.mod, например 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)иKVSetWithOptionsс параметромAtomic: безопасные обновления, когда несколько запросов или серверов записывают в один и тот же ключ. Приложение Polls сохраняет каждый голос таким образом и повторяет попытку при конфликте.KVList(page, perPage): ключи, постранично.
Веб-часть приложения
Бандл регистрирует класс с помощью window.registerPlugin. Его метод initialize получает реестр и хранилище Redux:
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());
- Общие библиотеки. Веб-приложение предоставляет React, ReactDOM, Redux, React Redux и React Intl: перечислите их как
externalsв webpack (React,ReactDOM,Redux,ReactRedux,ReactIntl), а не включайте собственные копии в бандл. См.webpack.config.jsприложения Polls. - Вызовы серверной части. Выполняйте запросы к
${window.basename}/plugins/<app id>/…с параметрами изClient4.getOptions()(изmattermost-redux/client): они содержат заголовки, необходимые серверу для принятия запроса от браузера. - Собственное состояние.
registerReducerдобавляет редьюсер, который хранится под ключом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 есть два веб-интерфейса: Fusion (по умолчанию) и классический. Оба загружают одни и те же бандлы приложений и используют один и тот же реестр, но Fusion самостоятельно отрисовывает многие элементы и размещает компоненты плагинов по-своему. Тестируйте приложение в обоих интерфейсах.
Определение интерфейса
Перед загрузкой любого приложения оба интерфейса устанавливают значение window.antimatterWebUI в 'fusion' или 'classic', а также то же значение в атрибут data-am-web-ui корневого элемента страницы. В Mattermost ни одно из этих значений не устанавливается.
export function isFusionUI(): boolean {
return window.antimatterWebUI === 'fusion' || document.documentElement.dataset.amWebUi === 'fusion';
}
Встроенные приложения используют это для отрисовки в собственном стиле. Приложение «Опросы» рисует свои карточки опросов и диалог создания в стиле Fusion, а для команды /poll в Fusion открывает собственный диалог. Приложения «Звонки» и «Голосовые каналы» не отображают боковую панель и всплывающие уведомления, которые Fusion заменяет собственным интерфейсом звонков. Приложение «Агенты» размещает свои действия с ИИ в меню сообщений, в группе под пунктом Другие действия, а не в панели инструментов, появляющейся при наведении. Собственные CSS-классы Fusion (с префиксом am-) и его слой диалоговых окон (элемент am-layer) — это то, что используют эти приложения для соответствия интерфейсу; это не задокументированный стабильный API.
Где Fusion отображает компоненты плагинов
| Метод реестра | Где это появляется в Fusion |
|---|---|
registerAppBarComponent, registerChannelHeaderButtonAction | Панель приложений — столбец значков на правом краю, между вашим аватаром и кнопкой Об Antimatter. Кнопки заголовка канала также размещаются здесь, а не в самом заголовке. Скрывается, если системный администратор включит Отключить панель приложений (Apps Bar) (ExperimentalSettings.DisableAppBar, по умолчанию выключено). |
registerRightHandSidebarComponent | Правая панель. Пользователи могут изменять её размер, перетаскивая край, и открыть её в новом окне. Если приложение также является продуктом, панель предлагает пункт Открыть на отдельной странице. См. Панель в новом окне. |
registerChannelHeaderIcon | Значки в заголовке канала, после кнопок звонков; на телефонах — в меню Ещё заголовка. |
registerCallButtonAction | Кнопка звонка в заголовке канала; несколько провайдеров разделяют меню Другие способы звонка. |
registerPostDropdownMenuAction | Меню Действия с сообщением (⋯) сообщения, в его подменю Другие действия; также в меню Действия с обсуждением панели обсуждения, под пунктом Другие действия. Fusion рисует значок приложения перед каждым действием, серым цветом: собственный значок для встроенных приложений, иначе значок, который приложение показывает в панели приложений, иначе нейтральный. Элемент без текста в начале текста действия (значок, который рисует само приложение) скрывается. |
registerPostDropdownSubMenuAction | Группа с заголовком в том же подменю Другие действия, после отдельных действий. Приложение не получает собственной записи в меню сообщения. |
registerPostActionComponent | Панель инструментов при наведении на сообщение, перед кнопкой ⋯. |
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponent | Само сообщение: его тело, под текстом, после времени, и вместо превью ссылок. |
registerChannelHeaderMenuAction | Меню Настройки канала (⋯ на канале в боковой панели, или правый клик), как пункты с значком разъёма. |
registerMainMenuAction | Меню сообщества (название сообщества вверху списка каналов), под пунктом Другие действия, сгруппированное по приложениям. |
registerSidebarBrowseOrAddChannelMenuAction | Меню сообщества, после Создать канал и Создать категорию. |
registerFileUploadMethod | Меню + поля ввода (Добавить к сообщению), после пункта Загрузить файл. |
registerPostEditorActionComponent | Панель форматирования поля ввода, когда форматирование отображается. |
registerAIActionMenuItemComponent | Меню ИИ поля ввода. |
registerFileDropdownMenuAction, registerFilePreviewComponent | Меню параметров файла и превью файла. |
registerUserSettings | Профиль и параметры, одна вкладка на каждое приложение под заголовком Плагины. |
registerChannelSettingsTab | Диалог параметров канала, под заголовком Приложения. |
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponent | Карточка профиля, отображаемая при клике на имя или аватар. |
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponent | Верх списка каналов сообщества, рядом с названием канала, значком канала и нижней частью боковой панели сообщества слева. |
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholder | Начало истории канала, верх списка сообщений, линия непрочитанных, над полем ввода и плейсхолдер поля ввода. |
registerChannelTypeOption | Типы каналов в диалоговом окне Создать канал. |
registerSearchComponents | Поле поиска в заголовке канала: области приложений отображаются, когда оно пустое. |
registerUserGuideDropdownMenuAction | Об Antimatter (внизу панели приложений), под пунктом Помощь. |
registerProduct | Полностраничные представления по URL продукта, перечисленные под пунктом Продукты в Об Antimatter. Страницы продуктов сохраняют классическую рамку страницы, чьё меню продуктов отображается только тогда, когда приложение регистрирует продукт или добавляет в него запись. Встроенные приложения Kanban и Playbooks больше не регистрируют продукты: их страницы открываются в их боковых панелях. |
registerRootComponent, registerGlobalComponent, хуки, такие как registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslations | Работают как в классическом интерфейсе. |
registerAdminConsolePlugin, registerAdminConsoleCustomSetting | Системная консоль, которая является классической в обоих интерфейсах. |
Что Fusion не отображает и не выполняет
registerPostDropdownMenuComponent(компонент в меню сообщения),registerPostCardTypeComponent(карточка сообщения в правой панели) иregisterProductSwitcherMenuItemне отображаются в основных представлениях Fusion. ИспользуйтеregisterPostDropdownMenuActionвместо первого.registerMessageWillBeUpdatedHookне выполняется при редактировании сообщения в Fusion. Проверяйте изменения на сервере с помощью хука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 и Memes используют его для добавления и управления 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), передавая состояние в качестве необязательного пятого аргумента. Так делают Заметки, Доска для рисования, Список дел, Kanban и Плейбуки.
fusionIcon указывает на иконку из набора иконок Fusion (например, poll, sticker, calendar или image; см. webapp/fusion/src/fusion/components/icon). Fusion отображает её вместо icon, чтобы элемент соответствовал остальным.
Параметры
settings_schema в plugin.json описывает страницу приложения в Системная консоль › Плагины:
"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 (отрисовывается компонентом, который веб-часть приложения регистрирует с помощью 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 языков, как и встроенные приложения. Веб-часть приложения делает это следующим образом:
- Записывайте каждую строку с помощью 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… - Передайте их веб-приложению, которое запрашивает язык пользователя:
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 | Собирает серверную часть для всех платформ и веб-приложение, а затем записывает пакет в 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 | Первая команда пересобирает веб-приложение при каждом сохранении; вторая упаковывает результат сборки и устанавливает его на сервер. |
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): выполняйте их на машине сервера с включённым локальным режимом. В противном случае они авторизуются на сервере, указанном в MM_SERVICESETTINGS_SITEURL, с помощью AM_ADMIN_TOKEN или AM_ADMIN_USERNAME и AM_ADMIN_PASSWORD. Инструменты сборки пока не читают имя AM_ для URL сайта.
Ручная установка пакета
- Системный администратор разрешает загрузку с помощью
AM_PLUGINSETTINGS_ENABLEUPLOADS=true(по умолчанию отключено; переключателя в Системной консоли нет). - Затем Системная консоль › Плагины › Управление плагинами › Загрузить плагин, либо выполните
amctl plugin add weather.tar.gz, а затемamctl plugin enable com.example.weather. - Если на сервере включён параметр Требовать подпись плагина, он принимает только пакеты, подписанные доверенным ключом; см. Установка других приложений.
AM_SERVICESETTINGS_ENABLEDEVELOPER=true: тогда ошибки будут содержать подробные сведения. Makefile встроенных приложений собирают серверную часть только для вашей платформы, если в вашей оболочке задана переменная MM_SERVICESETTINGS_ENABLEDEVELOPER.