Escritura de aplicaciones (plugins)
Las aplicaciones añaden funciones a Antimatter desde el interior: una parte de servidor en Go que se integra con el servidor, y una parte de aplicación web en React que añade menús, paneles, tipos de mensaje y páginas. Esta página muestra cómo se estructura una aplicación, usando la aplicación integrada de Encuestas como ejemplo, y qué cambia en la interfaz de Fusion de Antimatter.
Cómo funciona una aplicación
- La parte del servidor es un programa en Go. El servidor lo inicia como un proceso independiente y se comunica con él mediante RPC: llama a los hooks de la aplicación (un mensaje está a punto de publicarse, se ejecutó un comando, etc.) y la aplicación llama a la API de plugins del servidor (crear una publicación, leer un usuario, almacenar datos, etc.). También puede servir rutas HTTP bajo
/plugins/<app id>/. - La parte de la aplicación web es un paquete de JavaScript que se carga en el navegador junto con la aplicación web. Registra componentes y callbacks de React en el registro: un botón en un menú, un panel, una forma de dibujar un tipo de mensaje.
- Cada parte es opcional. Una app se instala como un paquete
.tar.gz, y los administradores del sistema la habilitan, deshabilitan y configuran.
Antimatter ejecuta las aplicaciones escritas para Mattermost, y viceversa, siempre que la aplicación solo use los hooks que ambos tienen. Consulte Hooks que solo tiene Antimatter.
El diseño del paquete
No existe una plantilla de inicio separada: empieza con una pequeña aplicación integrada como antimatter-plugin-polls o antimatter-plugin-todo, y cambia su ID y su código. Encuestas se estructura así:
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
Opcional: una carpeta public/, servida tal cual en /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: único y permanente; lo habitual es usar un nombre de dominio inverso. Nombra las rutas, la configuración almacenada y los eventos de WebSocket.version: versión semántica. Las aplicaciones incluidas no la incluyen enplugin.json; su herramienta de compilación la toma de la etiqueta gitv*más reciente.min_server_version: Antimatter informa12.0.0.settings_schema: la página de configuración de la app en la Consola de sistema (ver Configuración).
make apply copia el manifiesto en server/manifest.go y webapp/src/manifest.ts; make manifest-check lo valida.
La parte del servidor
La parte del servidor importa el mismo módulo Go que las apps de Mattermost, github.com/mattermost/mattermost/server/public, desde el proxy de módulos Go. Su main solo inicia la app:
package main
import "github.com/mattermost/mattermost/server/public/plugin"
func main() {
plugin.ClientMain(&Plugin{})
}
El tipo Plugin incrusta plugin.MattermostPlugin, lo que le proporciona p.API, e implementa los hooks que necesita:
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)
}
Hooks comunes
| Hook | Cuándo se ejecuta |
|---|---|
OnActivate, OnDeactivate | Cuando la app se habilita o deshabilita, y cuando el servidor se inicia y se detiene. Registra los comandos y los bots en OnActivate. |
OnConfigurationChange | Cuando cambian sus ajustes; cárgalos con p.API.LoadPluginConfiguration(&cfg). |
ServeHTTP | Solicitudes a /plugins/<app id>/…. El servidor establece la cabecera Mattermost-User-Id cuando la solicitud proviene de un usuario conectado; rechaza las solicitudes sin ella, a menos que estén destinadas a ser públicas. |
ExecuteCommand | Se ejecutó un comando registrado por la app. |
MessageWillBePosted, MessageWillBeUpdated | Antes de que se guarde un mensaje: devuelve un mensaje modificado o un motivo para rechazarlo. |
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeleted | Después del hecho, para efectos secundarios. |
ReactionHasBeenAdded, ReactionHasBeenRemoved | Reacciones. |
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreated | Cambios de membresía y de canal. Un equipo es una comunidad. |
FileWillBeUploaded | Antes de que se almacene un archivo: modifícalo o recházalo. |
OnPluginClusterEvent | Un mensaje de la misma app en otro servidor de un clúster. |
La lista completa, con las firmas exactas, está en server/public/plugin/hooks.go; la API está en server/public/plugin/api.go. El paquete pluginapi (server/public/pluginapi) envuelve la API en asistentes más amigables.
Llamadas de API útiles
CreatePost,UpdatePost,SendEphemeralPost: publicar como el bot de la app o como un usuario.EnsureBotUser(&model.Bot{Username, DisplayName}): crea el bot de la app una vez y obtiene su ID.PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): envía un evento a la app web. Llega comocustom_<app id>_poll_updated.OpenInteractiveDialog: abre un diálogo.LogInfo,LogErrory similares: escribe en el registro del servidor, etiquetado con el ID de la app.GetUser,GetChannel,HasPermissionToChannel: comprueba quién puede hacer qué antes de actuar en nombre de un usuario.
Una app se ejecuta en cada servidor de un clúster. Mantén el estado en el almacén de clave-valor o en la base de datos, no solo en memoria.
El servicio de IA
El servidor de Antimatter llama a modelos de IA para las aplicaciones, a través de las conexiones de modelos configuradas por un administrador (véase IA en la guía de administración). Una aplicación nunca conserva la clave de un proveedor: indica la persona en cuyo nombre actúa y el lugar, y el servidor verifica que esa persona pueda usar la conexión allí, aplica sus límites y presupuestos, cuenta los tokens en nombre de la persona y de la aplicación, y audita la llamada sin su contenido.
| Método | Qué hace |
|---|---|
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest) | Una finalización de chat: mensajes, herramientas, salida estructurada (JSONSchema). |
AIChatStream(…) | Lo mismo, en streaming: lee los Events del flujo hasta que el canal se cierre, o ciérralo con Close para detener la llamada. |
AIEmbed(…, *ai.EmbedRequest) | Incrustaciones (embeddings) de textos. |
AITranscribe(…, *ai.TranscribeRequest) | Voz a texto. |
AIDecide(…, *ai.DecideRequest) | Preguntas tipadas a un modelo de decisión (sí o no, una elección, una puntuación) sobre un texto. |
AIConnectionsForUser(userID, channelID, teamID) | Las conexiones que una persona puede usar allí, con sus modelos y capacidades, nunca sus claves. |
El ConnectionID de cada solicitud selecciona la conexión, y Model uno de sus modelos (vacío para el modelo predeterminado de la conexión). Operation es una etiqueta breve para los registros de uso y auditoría, nunca contenido. Su versión mínima de servidor es 11.11.
actingUserID solo de una fuente verificada por el servidor: la cabecera Mattermost-User-Id de una solicitud a tu ServeHTTP, o el usuario del contexto de un hook (el autor de una publicación, el usuario que ejecutó un comando). Nunca lo tomes de un mensaje, una carga de webhook, una URL o la solicitud de otra aplicación: tu aplicación gastaría el acceso de otra persona.Estos métodos y el paquete ai (server/public/ai) existen solo en la copia del módulo de Antimatter, no en la versión publicada por Mattermost. Para usarlos, compila contra server/public de Antimatter con una línea replace en tu go.mod, por ejemplo replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. Una aplicación así solo se ejecuta en Antimatter.
Proporcionar herramientas a los agentes
Una aplicación ofrece sus propias herramientas a los agentes mediante RegisterAITools([]*model.AIToolDefinition): el nombre, el título, la descripción, el esquema JSON de entrada y el tipo de cada herramienta (read, write o external_write), lo cual determina si la política de un agente permite ejecutarla por sí misma (consulte Herramientas en la guía de administración). Los permisos se verifican en un argumento de canal o comunidad de la llamada, o en el servidor, según lo que requiera la herramienta. Cuando un agente utiliza la herramienta, el servidor llama al hook ExecuteAITool(c, *model.AIToolExecution) *model.AIToolResult con la persona verificada que realizó la solicitud en UserId: actúe únicamente con sus permisos, nunca con los del agente. Las herramientas se nombran como <app id>.<name>; versión mínima del servidor 11.11; estos métodos y el hook solo existen en server/public de Antimatter, al igual que el servicio de IA anterior. Una aplicación cuya ID sea antimatter o que comience por mcp. no puede registrar herramientas.
Almacenamiento de datos
Cada app tiene su propio almacén de clave-valor en la base de datos del servidor:
KVSet(key, value),KVGet(key),KVDelete(key): los valores son bytes, generalmente JSON. Las claves tienen un máximo de 150 caracteres.KVSetWithExpiry(key, value, seconds): el valor desaparece después de ese tiempo.KVCompareAndSet(key, old, new)yKVSetWithOptionsconAtomic: actualizaciones seguras cuando varias solicitudes o servidores escriben la misma clave. Polls guarda cada voto de esta manera y reintenta en caso de conflicto.KVList(page, perPage): las claves, página por página.
La parte de la app web
El paquete registra una clase con window.registerPlugin. Su initialize recibe el registro y el almacén de 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());
- Bibliotecas compartidas. La app web proporciona React, ReactDOM, Redux, React Redux y React Intl: decláralas como
externalsde webpack (React,ReactDOM,Redux,ReactRedux,ReactIntl) en lugar de incluir tus propias copias. Consulta elwebpack.config.jsde Polls. - Llamadas a tu parte de servidor. Usa Fetch con
${window.basename}/plugins/<app id>/…y las opciones deClient4.getOptions()(demattermost-redux/client): estas llevan las cabeceras que el servidor necesita para aceptar una solicitud del navegador. - Tu propio estado.
registerReducerañade un reducer, almacenado bajostate['plugins-<app id>']. - Tipos. Los métodos adicionales del registro de Antimatter no están en los tipos publicados de Mattermost: Polls declara el registro que utiliza en
webapp/src/types/host.ts.
Los métodos del registro y sus parámetros están documentados en el código, en webapp/fusion/src/plugins/registry.ts (Fusion) y webapp/channels/src/plugins/registry.ts (clásico).
Soporte para la interfaz Fusion
Antimatter tiene dos interfaces web: Fusion, la predeterminada, y la clásica. Ambas cargan los mismos paquetes de apps y tienen el mismo registro, pero Fusion dibuja muchas cosas por sí misma y coloca los componentes de las apps de su propia manera. Prueba tu app en ambas.
Detectar la interfaz
Antes de que se cargue cualquier app, ambas interfaces establecen window.antimatterWebUI en 'fusion' o 'classic', y el mismo valor en el elemento raíz de la página como data-am-web-ui. En Mattermost, ninguno de los dos se establece.
export function isFusionUI(): boolean {
return window.antimatterWebUI === 'fusion' || document.documentElement.dataset.amWebUi === 'fusion';
}
Las aplicaciones incluidas utilizan esto para renderizar de manera diferente. Polls dibuja sus tarjetas de encuesta con el estilo de Fusion, y en Fusion la encuesta se escribe en el compositor (un modo de compositor) en lugar de en un cuadro de diálogo. Calls y Voice channels omiten los componentes de la barra lateral y de las notificaciones emergentes, que Fusion reemplaza por su propia interfaz de llamadas. La aplicación de chat de IA coloca sus acciones de IA en el menú de mensajes, como un grupo bajo Más Acciones, en lugar de en la barra de herramientas al pasar el cursor. Las clases CSS propias de Fusion (con el prefijo am-) y su capa de diálogos (el elemento am-layer) son lo que estas aplicaciones usan para coincidir con la interfaz; no son una API documentada ni estable.
Dónde Fusion muestra los componentes de los complementos
| Método del registro | Dónde aparece en Fusion |
|---|---|
registerAppBarComponent, registerChannelHeaderButtonAction | La barra de aplicaciones, la columna de iconos en el borde derecho, entre tu avatar y el botón Acerca de Antimatter. Los botones de la cabecera del canal también se encuentran allí, no en la cabecera del canal. Se oculta cuando un administrador del sistema activa Desactivar barra de aplicaciones (ExperimentalSettings.DisableAppBar, desactivado por defecto). |
registerRightHandSidebarComponent | El panel derecho. Su encabezado es el de Fusion, el mismo para todas las aplicaciones: el icono y el nombre de la aplicación (el título registrado cuando solo muestra texto, de lo contrario la etiqueta del riel de aplicaciones o el nombre del manifiesto), luego Restaurar el tamaño del panel (una vez redimensionado), Abrir en una nueva ventana y Cerrar. Un título que muestre cualquier otra cosa (un control, un enlace, una imagen) se descarta, por lo que debe dibujar los controles propios de su aplicación en el panel mismo, por ejemplo, en una fila delgada en la parte superior. Las personas pueden cambiar el tamaño del panel arrastrando su borde. Consulte El panel en una nueva ventana. |
registerChannelHeaderIcon | Iconos en la cabecera del canal, después de los botones de llamada; en los teléfonos, en el menú Más de la cabecera. |
registerCallButtonAction | El botón de llamada de la cabecera del canal; varios proveedores comparten un menú Otras formas de llamar. |
registerPostDropdownMenuAction | El menú Acciones de mensajes (⋯) del mensaje, en su submenú Más Acciones; también en el menú Acciones del hilo del panel de hilos, bajo Más Acciones. Fusion dibuja el icono de la aplicación antes de cada acción, en gris: su propio icono para las aplicaciones integradas, en caso contrario el icono que la aplicación muestra en la barra de aplicaciones, y si no, uno neutro. Un elemento sin texto al inicio del texto de la acción (un icono que la aplicación dibuja por sí misma) se oculta. |
registerPostDropdownSubMenuAction | Un grupo de entradas en el mismo submenú de Más Acciones, después de las acciones individuales, separadas por una línea y sin encabezado. Ninguna aplicación obtiene una entrada propia en el menú de mensajes. |
registerPostActionComponent | La barra de herramientas al pasar el cursor sobre el mensaje, antes del botón ⋯. |
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponent | El propio mensaje: su cuerpo, debajo de su texto, después de su hora y en lugar de las vistas previas de enlaces. |
registerChannelHeaderMenuAction | El menú Opciones de canal del canal (⋯ en un canal de la barra lateral, o clic derecho), como elementos con un icono de enchufe. |
registerMainMenuAction | El menú de la comunidad (el nombre de la comunidad en la parte superior de la lista de canales), bajo Más acciones, agrupado por aplicación. |
registerSidebarBrowseOrAddChannelMenuAction | El menú de la comunidad, después de Crear canal y Crear categoría. |
registerFileUploadMethod | El menú + del compositor (Agregar al mensaje), después de Subir un archivo. |
registerPostEditorActionComponent | La barra de formato del compositor, cuando se muestra el formato. |
registerAIActionMenuItemComponent | El menú IA del compositor. |
registerFileDropdownMenuAction, registerFilePreviewComponent | El menú de opciones de un archivo, y la vista previa del archivo. |
registerUserSettings | Perfil y configuración, una pestaña por aplicación bajo el encabezado Plugins. |
registerChannelSettingsTab | El diálogo de configuración del canal, bajo el encabezado Aplicaciones. |
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponent | La tarjeta de perfil que se muestra cuando haces clic en un nombre o un avatar. |
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponent | La parte superior de la lista de canales de una comunidad, junto al nombre del canal, el icono del canal y la parte inferior del panel de la comunidad, a la izquierda. |
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholder | El inicio del historial de un canal, la parte superior de la lista de mensajes, la línea de mensajes sin leer, sobre el editor de mensajes y el texto de relleno del editor. |
registerChannelTypeOption | Los tipos de canal del diálogo Crear canal. |
registerSearchComponents | La casilla de búsqueda de la cabecera del canal: los ámbitos de la aplicación se muestran cuando está vacía. |
registerUserGuideDropdownMenuAction | Acerca de Antimatter (parte inferior del panel de la aplicación), bajo Ayuda. |
registerProduct | Páginas completas en la URL del producto, en el marco de página clásico, sin un menú de producto: Fusion no tiene ninguno y no enumera productos en ninguna parte, por lo que debes enlazar tú mismo a la página. Ninguna aplicación incluida registra un producto: Kanban, Playbooks y el chat de IA se abren en sus paneles laterales. |
registerRootComponent, registerGlobalComponent, hooks como registerMessageWillBePostedHook, registerSlashCommandWillBePostedHook, registerMessageWillFormatHook, registerFilesWillUploadHook, registerDesktopNotificationHook, registerWebSocketEventHandler, registerReducer, registerTranslations | Funcionan como en la interfaz clásica. |
registerAdminConsolePlugin, registerAdminConsoleCustomSetting | La Consola de sistema, que es la clásica en ambas interfaces. |
Lo que Fusion no muestra ni ejecuta
registerPostDropdownMenuComponent(un componente en el menú del mensaje) yregisterPostCardTypeComponent(la tarjeta de una publicación en el panel derecho) no se muestran en las vistas principales de Fusion.registerProductSwitcherMenuItemno hace nada en Fusion, que no tiene menú de producto (registra una advertencia). UsaregisterPostDropdownMenuActionen lugar del primero.registerMessageWillBeUpdatedHookno se ejecuta cuando se edita un mensaje en Fusion. Compruebe las ediciones en el servidor con el hookMessageWillBeUpdateden su lugar.registerActionAfterChannelCreationsolo se ejecuta desde el diálogo clásico de creación de canal, que Fusion abre desde Más opciones… en su diálogo Crear canal.- Los componentes de la barra de la aplicación se muestran independientemente de su
supportedProductIds. - Las rutas de
registerNeedsTeamRouteyregisterCustomRouteconservan el marco de página clásico, al igual que los productos.
Hooks que solo tiene Antimatter
Estos métodos de registro no existen en Mattermost. Compruebe que un método existe antes de llamarlo, para que la aplicación también funcione allí:
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');
}
| Método | Qué añade |
|---|---|
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender}) | Un elemento en el menú + de la caja de mensajes, en ambas interfaces. action recibe {channelId, rootId}. |
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker}) | Una pestaña del selector de emojis de la caja de mensajes, como GIFs o pegatinas. En Fusion, las pestañas con un order menor que 10 aparecen antes de la pestaña Emoji. El componente recibe channelId, rootId, filter, onFilterChange, onSelectDone, insertText, isFusion y, en Fusion, pickerKit (ver abajo). |
registerChannelViewPanel({matcher, component}) | Un panel entre la cabecera del canal y los mensajes, para los canales que selecciona matcher(state, channel). Puede ocultar los mensajes y el compositor (setMessagesVisible), como hacen los canales de voz. |
registerSidebarChannelFooterComponent(component) | Una línea bajo un canal en la barra lateral, como las personas en su llamada. Recibe el channel. |
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender}) | Solo Fusion: una entrada en las páginas Integraciones, junto a los emojis personalizados. La app de GIF añade sus GIF y pegatinas personalizados allí. |
Agregar y gestionar vistas en el selector de emojis
En Fusion, una pestaña del selector de emojis recibe la prop pickerKit: los componentes que la pestaña Emoji utiliza para sus propias vistas de adición y gestión, de modo que las vistas de una pestaña tengan el mismo aspecto y se mantengan dentro del selector. Las aplicaciones GIFs y Memes la utilizan para añadir y gestionar GIFs, pegatinas y plantillas de memes. Contiene version (1), los hooks usePickerView y useOnEscape, y los componentes PickerViewButtons (los botones + y de gestión junto a la casilla de búsqueda), PickerViewFrame (una vista con su flecha de retroceso), PickerNotice, PickerField, PickerSearch, PickerDropZone y PickerManageList. La tecla Escape retrocede desde una vista a la cuadrícula de la pestaña. Comprueba que la prop esté presente: otros hosts la omiten, y en ese caso la pestaña mantiene sus propios diálogos. Los tipos están en webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps) y los componentes en webapp/fusion/src/fusion/popovers/picker_views.tsx.
Modos del compositor
En Fusion, registerComposerMode({id, label, fusionIcon, placeholder, sendLabel, component, shouldRender}) añade otra forma de escribir un mensaje al compositor, como hace Polls para una encuesta. El modo se ofrece en el menú + del compositor bajo su label. Una vez seleccionado, el compositor mantiene su cuadro de texto (con el placeholder del modo: la pregunta de la encuesta), muestra tu component debajo y una barra que nombra el modo con un botón Cancelar; Enviar toma el sendLabel y ejecuta tu envío en lugar de publicar el texto.
- El componente recibe
channelId,rootId(el hilo, en un cuadro de respuesta),message(el texto del compositor),setSubmityonCancel(vuelve a un mensaje normal, conservando el texto). - Pasa a
setSubmituna función que se llama con el texto del compositor y se resuelve a nada (o una cadena vacía) una vez enviado: el compositor se vacía entonces y vuelve a un mensaje normal. Resuelve a una cadena para mostrarla como un error en su lugar. placeholderysendLabelson cadenas, o funciones deintlde react-intl.shouldRender(state, {channelId, rootId})decide si un compositor ofrece el modo.- Para abrir el modo desde otro lugar, como un comando con barra diagonal sin argumentos:
window.dispatchEvent(new CustomEvent('am-composer-mode', {detail: {id, channelId, rootId}})).
Solo Fusion tiene modos de compositor: comprueba que el registro tiene la función, y mantén tu propio diálogo en caso contrario. El método devuelve el id del modo, y el modo desaparece cuando se elimina la aplicación.
El panel en una nueva ventana
Fusion da al componente de registerRightHandSidebarComponent dos props más, para que Abrir en una nueva ventana abra el panel donde estaba en lugar de en la página principal de la aplicación:
setPanelState(state): llámalo cuando el panel cambie de lugar, con una cadena corta definida por tu aplicación, como'note/<id>'o'board/<id>/card/<id>', onullen su página de inicio. Se incluye en la dirección de la ventana, por lo que nunca debes incluir información confidencial en ella. Un estado de más de 512 caracteres, o que contenga caracteres de control, se descarta.initialPanelState: en la nueva ventana, el estado que tenía el panel, oundefined. Trátalo como una entrada no confiable: vuelve a obtener el elemento como la persona iniciada sesión y muestra la página de inicio de la aplicación o una vista de «no encontrado» si no puede abrirlo.
El estado se olvida cuando el panel se cierra o cuando el panel de otra aplicación ocupa su lugar. Para abrir la ventana de un panel por tu cuenta, popoutRhsPlugin (también en window.WebappUtils.popouts) acepta el estado como un quinto argumento opcional. Notas, Pizarra, Por hacer, Kanban y Playbooks utilizan esto.
fusionIcon nombra un icono del conjunto de iconos de Fusion (por ejemplo poll, sticker, calendar o image; ver webapp/fusion/src/fusion/components/icon). Fusion lo muestra en lugar de icon, para que el elemento coincida con los demás.
Configuración
settings_schema en plugin.json describe la página de la app en Consola de sistema › Plugins:
"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."}
]
}
Tipos de configuración: bool, text, longtext, number, dropdown, radio, generated (un secreto aleatorio), username y custom (dibujado por un componente que la parte de la app web registra con registerAdminConsoleCustomSetting). Las configuraciones se pueden agrupar en sections.
La parte del servidor las lee en una estructura cuyos campos coinciden con las claves:
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
}
Los administradores también pueden establecerlas en la configuración, bajo PluginSettings.Plugins.<app id>, con claves en minúsculas (ver la guía de administración).
Traducciones
Antimatter está traducido a 37 idiomas, y las apps incluidas también. La parte de la app web lo hace así:
- Escribe cada cadena con React Intl, con un ID y su texto en inglés:
<FormattedMessage id='polls.create' defaultMessage='Create a poll'/>odefineMessages. Prefija los IDs con el nombre de tu app. - Extrae las cadenas en inglés a
webapp/i18n/en.json(las apps incluidas usanformatjs extract). - Añade un archivo por idioma junto a él, con los mismos IDs:
fr.json,de.json,pt-BR.json,zh-CN.json… - Pásalas a la aplicación web, que solicita el idioma del usuario:
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);
Si falta una cadena, se usa su defaultMessage en inglés. Para las cadenas que envía la parte del servidor (respuestas de comandos, mensajes de bots), el paquete pluginapi/i18n carga los archivos de traducción incluidos en el paquete (por ejemplo, assets/i18n) y proporciona un localizador en el idioma de cada usuario.
Compilar, instalar y depurar
Necesitas Go y Node.js (la versión indicada en el .nvmrc de la aplicación). En la carpeta de la aplicación:
| Comando | Qué hace |
|---|---|
make dist | Compila la parte del servidor para todas las plataformas y la parte de la aplicación web, y escribe el paquete en dist/<app id>-<version>.tar.gz. |
make test | Ejecuta las pruebas de Go y JavaScript. |
make check-style | Analiza el código (golangci-lint, ESLint) y comprueba los tipos de TypeScript. |
make deploy | Compila, luego instala y habilita el paquete en un servidor. |
make watch, make deploy-from-watch | El primero recompila la parte de la aplicación web cada vez que guardas; el segundo empaqueta lo que compiló y lo instala en el servidor. |
make enable, make disable, make reset | Habilita, deshabilita o reinicia la aplicación en el servidor. |
make logs, make logs-watch | Muestra las líneas de la aplicación en el registro del servidor. |
make attach | Adjunta el depurador Delve a la parte del servidor en ejecución (compila primero con AM_DEBUG=1). |
make deploy y los otros comandos del servidor usan un socket de modo local cuando lo encuentran (/var/tmp/antimatter_local.socket, o AM_LOCALSOCKETPATH): ejecútalos en la máquina del servidor con el modo local activado. De lo contrario, inician sesión en el servidor indicado por MM_SERVICESETTINGS_SITEURL con AM_ADMIN_TOKEN, o con AM_ADMIN_USERNAME y AM_ADMIN_PASSWORD. Las herramientas de compilación aún no leen un nombre AM_ para la URL del sitio.
Instalar un paquete manualmente
- Un administrador del sistema permite las subidas con
AM_PLUGINSETTINGS_ENABLEUPLOADS=true(desactivado por defecto; no hay un interruptor en la Consola de sistema). - Luego Consola de sistema › Plugins › Gestión de Plugins › Cargar Plugin, o
amctl plugin add weather.tar.gzseguido deamctl plugin enable com.example.weather. - Si el servidor tiene activado Requerir Firma del Plugin, solo acepta paquetes firmados con una clave de confianza; consulte Instalación de otras aplicaciones.
AM_SERVICESETTINGS_ENABLEDEVELOPER=true en el servidor: los errores entonces incluyen sus detalles. Los Makefiles de las aplicaciones incluidas solo compilan la parte del servidor para tu plataforma cuando MM_SERVICESETTINGS_ENABLEDEVELOPER está definido en tu shell.