撰寫應用程式(外掛程式)

應用程式從內部為 Antimatter 新增功能:一個以 Go 撰寫、掛載到伺服器的伺服器端,以及一個以 React 撰寫、新增選單、面板、訊息類型與頁面的 Web 應用程式端。本頁以隨附的投票(Polls)應用程式為例,展示應用程式的結構,以及 Antimatter 的 Fusion 介面有何不同。

應用程式的運作方式

  • 伺服器端是一個 Go 程式。伺服器會將其啟動為獨立程序,並透過 RPC 與其通訊:伺服器呼叫應用程式的 hooks(例如訊息即將發布、指令已執行……),應用程式則呼叫伺服器的 plugin API(建立貼文、讀取使用者、儲存資料……)。它也可以在 /plugins/<app id>/ 下提供 HTTP 路由。
  • Web 應用程式端是一個 JavaScript bundle,會與 Web 應用程式一起載入瀏覽器。它透過 註冊表 註冊 React 元件與回呼:選單中的按鈕、面板、繪製訊息類型的方式。
  • 兩端皆為選填。應用程式以 .tar.gz bundle 形式安裝,由系統管理員啟用、停用並設定。
伺服器端擁有對伺服器資料的完全存取權限。請將應用程式視為部署到伺服器的程式碼。

Antimatter 可執行為 Mattermost 撰寫的應用程式,反之亦然,只要應用程式僅使用兩者都有的 hooks。請參閱 Antimatter 獨有的 Hooks。

Bundle 結構

沒有獨立的起始範本:請從小型的隨附應用程式開始,例如 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 中省略;其建置工具會從最新的 v* git tag 取得。
  • min_server_version:Antimatter 報告 12.0.0。
  • settings_schema:系統控制台中應用程式的設定頁面(請參閱設定)。

make apply 會將 manifest 複製到 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,並實現所需的 hooks:

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

Hook執行時機
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:在代表使用者執行操作之前,檢查誰可以執行什麼。

應用程式會執行在叢集的每台伺服器上。請將狀態儲存在金鑰-值儲存區或資料庫中,而不是只放在記憶體裡。

AI 服務

Antimatter 的伺服器會透過管理員設定的模型連線(請參閱管理員指南中的 AI)為應用程式呼叫 AI 模型。應用程式絕不持有提供者的金鑰:它只需指定代表誰、在何處執行,伺服器會檢查該使用者是否能在該處使用此連線,套用其限制與預算,將 token 計入該使用者與應用程式,並稽核呼叫內容(不含實際內容)。

方法功能說明
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)聊天補全:訊息、工具、結構化輸出(JSONSchema)。
AIChatStream(…)相同功能,但採用串流方式:讀取串流的 Events 直到頻道關閉,或呼叫 Close 以停止呼叫。
AIEmbed(…, *ai.EmbedRequest)文字嵌入(Embeddings)。
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 發布的版本。若要使用它們,請針對 Antimatter 的 server/public 進行建置,並在您的 go.mod 中添加一行 replace,例如 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 應用程式部分

套件透過 window.registerPlugin 註冊一個類別。其 initialize 方法會接收註冊表(registry)和 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)中的選項,Fetch ${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 和經典版。兩者載入相同的應用程式套件並擁有相同的註冊表,但 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:在整合頁面上,自訂表情符號旁邊的項目。GIFs 應用程式會將其自訂 GIF 和貼紙新增到該處。

在表情符號選擇器中新增及管理檢視

在 Fusion 中,表情符號選擇器分頁會收到一個 pickerKit 屬性:這是表情符號分頁用於其新增與管理檢視的元件,讓分頁的檢視保持一致並保持在選擇器內。GIFs 和 Memes 應用程式使用它來新增和管理 GIF、貼紙和迷因模板。它包含 version (1)、鉤子 usePickerView 和 useOnEscape,以及元件 PickerViewButtons(搜尋框旁邊的 + 和管理按鈕)、PickerViewFrame(帶有返回箭頭的檢視)、PickerNotice、PickerField、PickerSearch、PickerDropZone 和 PickerManageList。按 Esc 鍵會從檢視返回到分頁的網格。請確認該屬性存在:其他宿主會省略它,此時分頁會保留自己的對話框。類型定義位於 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 套件會載入隨 bundle 提供的翻譯檔案(例如 assets/i18n),並為每位使用者提供對應語言的 localizer。

建置、安裝與除錯

您需要 Go 和 Node.js(版本請參照應用程式的 .nvmrc)。在應用程式的資料夾中執行:

指令功能說明
make dist為所有平台建置伺服器端,並建置 Web 應用程式端,然後將 bundle 寫入 dist/<app id>-<version>.tar.gz。
make test執行 Go 和 JavaScript 測試。
make check-style檢查程式碼(golangci-lint、ESLint)並驗證 TypeScript 型別。
make deploy建置後,在伺服器上安裝並啟用該 bundle。
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 和其他伺服器指令在找到本地模式 socket 時會使用它(/var/tmp/antimatter_local.socket,或 AM_LOCALSOCKETPATH):請在伺服器機器上開啟本地模式並執行這些指令。否則,它們會使用 AM_ADMIN_TOKEN,或 AM_ADMIN_USERNAME 和 AM_ADMIN_PASSWORD 登入由 MM_SERVICESETTINGS_SITEURL 指定的伺服器。建置工具目前尚未讀取用於網站 URL 的 AM_ 變數。

手動安裝 bundle

  • 系統管理員可透過 AM_PLUGINSETTINGS_ENABLEUPLOADS=true 允許上傳(預設為關閉;系統控制台中沒有對應的開關)。
  • 接著前往系統控制台 › 擴充程式 › 擴充程式管理 › 上傳擴充程式,或執行amctl plugin add weather.tar.gz,然後執行amctl plugin enable com.example.weather。
  • 如果伺服器啟用了擴充程式需經簽署,它只接受使用其信任金鑰簽署的套件;請參閱安裝其他應用程式。
為了在開發伺服器上進行快速迭代,請在伺服器上設定 AM_SERVICESETTINGS_ENABLEDEVELOPER=true:這樣錯誤訊息就會包含詳細資訊。當您的 shell 中設定了 MM_SERVICESETTINGS_ENABLEDEVELOPER 時,隨附應用程式的 Makefile 只會為您的平台建置伺服器端。